/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.qpid.proton.engine.impl;

import static org.apache.qpid.proton.engine.impl.AmqpHeader.HEADER;
import static org.apache.qpid.proton.engine.impl.TransportTestHelper.stringOfLength;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.spy;

import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.atomic.AtomicReference;

import org.apache.qpid.proton.Proton;
import org.apache.qpid.proton.amqp.Binary;
import org.apache.qpid.proton.amqp.Symbol;
import org.apache.qpid.proton.amqp.UnsignedInteger;
import org.apache.qpid.proton.amqp.UnsignedShort;
import org.apache.qpid.proton.amqp.messaging.Accepted;
import org.apache.qpid.proton.amqp.messaging.AmqpValue;
import org.apache.qpid.proton.amqp.messaging.Released;
import org.apache.qpid.proton.amqp.transport.AmqpError;
import org.apache.qpid.proton.amqp.transport.Attach;
import org.apache.qpid.proton.amqp.transport.Begin;
import org.apache.qpid.proton.amqp.transport.Close;
import org.apache.qpid.proton.amqp.transport.ConnectionError;
import org.apache.qpid.proton.amqp.transport.Detach;
import org.apache.qpid.proton.amqp.transport.Disposition;
import org.apache.qpid.proton.amqp.transport.End;
import org.apache.qpid.proton.amqp.transport.ErrorCondition;
import org.apache.qpid.proton.amqp.transport.Flow;
import org.apache.qpid.proton.amqp.transport.FrameBody;
import org.apache.qpid.proton.amqp.transport.Open;
import org.apache.qpid.proton.amqp.transport.Role;
import org.apache.qpid.proton.amqp.transport.Transfer;
import org.apache.qpid.proton.codec.ReadableBuffer;
import org.apache.qpid.proton.engine.Collector;
import org.apache.qpid.proton.engine.Connection;
import org.apache.qpid.proton.engine.Delivery;
import org.apache.qpid.proton.engine.EndpointState;
import org.apache.qpid.proton.engine.Event;
import org.apache.qpid.proton.engine.Link;
import org.apache.qpid.proton.engine.Receiver;
import org.apache.qpid.proton.engine.Sender;
import org.apache.qpid.proton.engine.Session;
import org.apache.qpid.proton.engine.Transport;
import org.apache.qpid.proton.engine.TransportException;
import org.apache.qpid.proton.framing.TransportFrame;
import org.apache.qpid.proton.message.Message;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;

public class TransportImplTest
{
    private TransportImpl _transport = new TransportImpl();

    private static final int CHANNEL_ID = 1;
    private static final TransportFrame TRANSPORT_FRAME_BEGIN = new TransportFrame(CHANNEL_ID, new Begin(), null);
    private static final TransportFrame TRANSPORT_FRAME_OPEN = new TransportFrame(CHANNEL_ID, new Open(), null);

    private static final int BUFFER_SIZE = 8 * 1024;

    @Rule
    public ExpectedException _expectedException = ExpectedException.none();

    @Test
    public void testInput()
    {
        ByteBuffer buffer = _transport.getInputBuffer();
        buffer.put(HEADER);
        _transport.processInput().checkIsOk();

        assertNotNull(_transport.getInputBuffer());
    }

    @Test
    public void testInitialProcessIsNoop()
    {
        _transport.process();
    }

    @Test
    public void testProcessIsIdempotent()
    {
        _transport.process();
        _transport.process();
    }

    /**
     * Empty input is always allowed by {@link Transport#getInputBuffer()} and
     * {@link Transport#processInput()}, in contrast to the old API.
     *
     * @see TransportImplTest#testEmptyInputBeforeBindUsingOldApi_causesTransportException()
     */
    @Test
    public void testEmptyInput_isAllowed()
    {
        _transport.getInputBuffer();
        _transport.processInput().checkIsOk();
    }

    /**
     * Tests the end-of-stream behaviour specified by {@link Transport#input(byte[], int, int)}.
     */
    @Test
    public void testEmptyInputBeforeBindUsingOldApi_causesTransportException()
    {
        _expectedException.expect(TransportException.class);
        _expectedException.expectMessage("Unexpected EOS when remote connection not closed: connection aborted");
        _transport.input(new byte [0], 0, 0);
    }

    /**
     * TODO it's not clear why empty input is specifically allowed in this case.
     */
    @Test
    public void testEmptyInputWhenRemoteConnectionIsClosedUsingOldApi_isAllowed()
    {
        ConnectionImpl connection = new ConnectionImpl();
        _transport.bind(connection);
        connection.setRemoteState(EndpointState.CLOSED);
        _transport.input(new byte [0], 0, 0);
    }

    @Test
    public void testOutupt()
    {
        {
            // TransportImpl's underlying output spontaneously outputs the AMQP header
            final ByteBuffer outputBuffer = _transport.getOutputBuffer();
            assertEquals(HEADER.length, outputBuffer.remaining());

            byte[] outputBytes = new byte[HEADER.length];
            outputBuffer.get(outputBytes);
            assertArrayEquals(HEADER, outputBytes);

            _transport.outputConsumed();
        }

        {
            final ByteBuffer outputBuffer = _transport.getOutputBuffer();
            assertEquals(0, outputBuffer.remaining());
            _transport.outputConsumed();
        }
    }

    @Test
    public void testOutputBufferIsReadOnly()
    {
        doTestTransportBufferReadability(true, false);
    }

    @Test
    public void testOutputBufferNotReadOnlyWhenConfigured()
    {
        doTestTransportBufferReadability(false, false);
    }

    @Test
    public void testHeadIsReadOnly()
    {
        doTestTransportBufferReadability(true, true);
    }

    @Test
    public void testHeadNotReadOnlyWhenConfigured()
    {
        doTestTransportBufferReadability(false, true);
    }

    private void doTestTransportBufferReadability(boolean readOnly, boolean headOrOutput)
    {
        TransportImpl transport = new TransportImpl();

        // Default should be Read-Only
        if (!readOnly) {
            transport.setUseReadOnlyOutputBuffer(readOnly);
        }

        final ByteBuffer outputBuffer;
        if (headOrOutput) {
            outputBuffer = transport.head();
        } else {
            outputBuffer = transport.getOutputBuffer();
        }

        assertTrue(outputBuffer.hasRemaining());
        if (readOnly) {
            assertTrue(outputBuffer.isReadOnly());
        } else {
            assertFalse(outputBuffer.isReadOnly());
        }

        byte[] outputBytes = new byte[outputBuffer.remaining()];
        outputBuffer.get(outputBytes);

        transport.outputConsumed();

        final ByteBuffer emptyBuffer;
        if (headOrOutput) {
            emptyBuffer = transport.head();
        } else {
            emptyBuffer = transport.getOutputBuffer();
        }

        assertFalse(emptyBuffer.hasRemaining());
        if (readOnly) {
            assertTrue(emptyBuffer.isReadOnly());
        } else {
            assertFalse(emptyBuffer.isReadOnly());
        }
    }

    @Test
    public void testTransportInitiallyHandlesFrames()
    {
        assertTrue(_transport.isHandlingFrames());
    }

    @Test
    public void testBoundTransport_continuesToHandleFrames()
    {
        Connection connection = new ConnectionImpl();

        assertTrue(_transport.isHandlingFrames());

        _transport.bind(connection);

        assertTrue(_transport.isHandlingFrames());

        _transport.handleFrame(TRANSPORT_FRAME_OPEN);

        assertTrue(_transport.isHandlingFrames());
    }

    @Test
    public void testUnboundTransport_stopsHandlingFrames()
    {
        assertTrue(_transport.isHandlingFrames());

        _transport.handleFrame(TRANSPORT_FRAME_OPEN);

        assertFalse(_transport.isHandlingFrames());
    }

    @Test
    public void testHandleFrameWhenNotHandling_throwsIllegalStateException()
    {
        assertTrue(_transport.isHandlingFrames());

        _transport.handleFrame(TRANSPORT_FRAME_OPEN);

        assertFalse(_transport.isHandlingFrames());

        _expectedException.expect(IllegalStateException.class);
        _transport.handleFrame(TRANSPORT_FRAME_BEGIN);
    }

    @Test
    public void testOutputTooBigToBeWrittenInOneGo()
    {
        int smallMaxFrameSize = 512;
        _transport = new TransportImpl(smallMaxFrameSize);

        Connection conn = new ConnectionImpl();
        _transport.bind(conn);

        // Open frame sized in order to produce a frame that will almost fill output buffer
        conn.setHostname(stringOfLength("x", 500));
        conn.open();

        // Close the connection to generate a Close frame which will cause an overflow
        // internally - we'll get the remaining bytes on the next interaction.
        conn.close();

        ByteBuffer buf = _transport.getOutputBuffer();
        assertEquals("Expecting buffer to be full", smallMaxFrameSize, buf.remaining());
        buf.position(buf.limit());
        _transport.outputConsumed();

        buf  = _transport.getOutputBuffer();
        assertTrue("Expecting second buffer to have bytes", buf.remaining() > 0);
        assertTrue("Expecting second buffer to not be full", buf.remaining() < Transport.MIN_MAX_FRAME_SIZE);
    }

    @Test
    public void testAttemptToInitiateSaslAfterProcessingBeginsCausesIllegalStateException()
    {
        _transport.process();

        try
        {
            _transport.sasl();
        }
        catch(IllegalStateException ise)
        {
            //expected, sasl must be initiated before processing begins
        }
    }

    @Test
    public void testChannelMaxDefault() throws Exception
    {
        Transport transport = Proton.transport();

        assertEquals("Unesxpected value for channel-max", 65535, transport.getChannelMax());
    }

    @Test
    public void testSetGetChannelMax() throws Exception
    {
        Transport transport = Proton.transport();

        int channelMax = 456;
        transport.setChannelMax(channelMax);
        assertEquals("Unesxpected value for channel-max", channelMax, transport.getChannelMax());
    }

    @Test
    public void testSetChannelMaxOutsideLegalUshortRangeThrowsIAE() throws Exception
    {
        Transport transport = Proton.transport();

        try {
            transport.setChannelMax( 1 << 16);
            fail("Expected exception to be thrown");
        } catch (IllegalArgumentException iae ){
            // Expected
        }

        try {
            transport.setChannelMax(-1);
            fail("Expected exception to be thrown");
        } catch (IllegalArgumentException iae ){
            // Expected
        }
    }

    private class MockTransportImpl extends TransportImpl
    {
        public MockTransportImpl() {
            super();
        }

        public MockTransportImpl(int maxFrameSize) {
            super(maxFrameSize);
        }

        LinkedList<FrameBody> writes = new LinkedList<FrameBody>();

        @Override
        protected void writeFrame(int channel, FrameBody frameBody,
                                  ReadableBuffer payload, Runnable onPayloadTooLarge) {
            super.writeFrame(channel, frameBody, payload, onPayloadTooLarge);
            writes.addLast(frameBody != null ? frameBody.copy() : null);
        }
    }

    @Test
    public void testTickRemoteTimeout()
    {
        MockTransportImpl transport = new MockTransportImpl();
        Connection connection = Proton.connection();
        transport.bind(connection);

        int timeout = 4000;
        Open open = new Open();
        open.setIdleTimeOut(new UnsignedInteger(4000));
        TransportFrame openFrame = new TransportFrame(CHANNEL_ID, open, null);
        transport.handleFrame(openFrame);
        pumpMockTransport(transport);

        long deadline = transport.tick(0);
        assertEquals("Expected to be returned a deadline of 2000",  2000, deadline);  // deadline = 4000 / 2

        deadline = transport.tick(1000);    // Wait for less than the deadline with no data - get the same value
        assertEquals("When the deadline hasn't been reached tick() should return the previous deadline",  2000, deadline);
        assertEquals("When the deadline hasn't been reached tick() shouldn't write data", 0, transport.writes.size());

        deadline = transport.tick(timeout/2); // Wait for the deadline - next deadline should be (4000/2)*2
        assertEquals("When the deadline has been reached expected a new deadline to be returned 4000",  4000, deadline);
        assertEquals("tick() should have written data", 1, transport.writes.size());
        assertEquals("tick() should have written an empty frame", null, transport.writes.get(0));

        transport.writeFrame(CHANNEL_ID, new Begin(), null, null);
        while(transport.pending() > 0) transport.pop(transport.head().remaining());
        int framesWrittenBeforeTick = transport.writes.size();
        deadline = transport.tick(3000);
        assertEquals("Writing data resets the deadline",  5000, deadline);
        assertEquals("When the deadline is reset tick() shouldn't write an empty frame", 0, transport.writes.size() - framesWrittenBeforeTick);

        transport.writeFrame(CHANNEL_ID, new Attach(), null, null);
        assertTrue(transport.pending() > 0);
        framesWrittenBeforeTick = transport.writes.size();
        deadline = transport.tick(4000);
        assertEquals("Having pending data does not reset the deadline",  5000, deadline);
        assertEquals("Having pending data prevents tick() from sending an empty frame", 0, transport.writes.size() - framesWrittenBeforeTick);
    }

    @Test
    public void testTickLocalTimeout()
    {
        MockTransportImpl transport = new MockTransportImpl();
        transport.setIdleTimeout(4000);
        Connection connection = Proton.connection();
        transport.bind(connection);

        transport.handleFrame(TRANSPORT_FRAME_OPEN);
        connection.open();
        pumpMockTransport(transport);

        long deadline = transport.tick(0);
        assertEquals("Expected to be returned a deadline of 4000",  4000, deadline);

        int framesWrittenBeforeTick = transport.writes.size();
        deadline = transport.tick(1000);    // Wait for less than the deadline with no data - get the same value
        assertEquals("When the deadline hasn't been reached tick() should return the previous deadline",  4000, deadline);
        assertEquals("Reading data should never result in a frame being written", 0, transport.writes.size() - framesWrittenBeforeTick);

        // Protocol header + empty frame
        ByteBuffer data = ByteBuffer.wrap(new byte[] {'A', 'M', 'Q', 'P', 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x02, 0x00, 0x00, 0x00});
        processInput(transport, data);
        framesWrittenBeforeTick = transport.writes.size();
        deadline = transport.tick(2000);
        assertEquals("Reading data data resets the deadline",  6000, deadline);
        assertEquals("Reading data should never result in a frame being written", 0, transport.writes.size() - framesWrittenBeforeTick);
        assertEquals("Reading data before the deadline should keep the connection open", EndpointState.ACTIVE, connection.getLocalState());

        framesWrittenBeforeTick = transport.writes.size();
        deadline = transport.tick(7000);
        assertEquals("Calling tick() after the deadline should result in the connection being closed", EndpointState.CLOSED, connection.getLocalState());
    }

    /*
     * No frames should be written until the Connection object is
     * opened, at which point the Open, and Begin frames should
     * be pipelined together.
     */
    @Test
    public void testOpenSessionBeforeOpenConnection()
    {
        MockTransportImpl transport = new MockTransportImpl();
        Connection connection = Proton.connection();
        transport.bind(connection);

        Session session = connection.session();
        session.open();

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 0, transport.writes.size());

        connection.open();

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 2, transport.writes.size());

        assertTrue("Unexpected frame type", transport.writes.get(0) instanceof Open);
        assertTrue("Unexpected frame type", transport.writes.get(1) instanceof Begin);
    }

    /*
     * No frames should be written until the Connection object is
     * opened, at which point the Open, Begin, and Attach frames
     * should be pipelined together.
     */
    @Test
    public void testOpenReceiverBeforeOpenConnection()
    {
        doOpenLinkBeforeOpenConnectionTestImpl(true);
    }

    /**
     * No frames should be written until the Connection object is
     * opened, at which point the Open, Begin, and Attach frames
     * should be pipelined together.
     */
    @Test
    public void testOpenSenderBeforeOpenConnection()
    {
        doOpenLinkBeforeOpenConnectionTestImpl(false);
    }

    void doOpenLinkBeforeOpenConnectionTestImpl(boolean receiverLink)
    {
        MockTransportImpl transport = new MockTransportImpl();
        Connection connection = Proton.connection();
        transport.bind(connection);

        Session session = connection.session();
        session.open();

        Link link = null;
        if(receiverLink)
        {
            link = session.receiver("myReceiver");
        }
        else
        {
            link = session.sender("mySender");
        }
        link.open();

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 0, transport.writes.size());

        // Now open the connection, expect the Open, Begin, and Attach frames
        connection.open();

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 3, transport.writes.size());

        assertTrue("Unexpected frame type", transport.writes.get(0) instanceof Open);
        assertTrue("Unexpected frame type", transport.writes.get(1) instanceof Begin);
        assertTrue("Unexpected frame type", transport.writes.get(2) instanceof Attach);
    }

    /*
     * No attach frame should be written before the Session begin is sent.
     */
    @Test
    public void testOpenReceiverBeforeOpenSession()
    {
        doOpenLinkBeforeOpenSessionTestImpl(true);
    }

    /*
     * No attach frame should be written before the Session begin is sent.
     */
    @Test
    public void testOpenSenderBeforeOpenSession()
    {
        doOpenLinkBeforeOpenSessionTestImpl(false);
    }

    void doOpenLinkBeforeOpenSessionTestImpl(boolean receiverLink)
    {
        MockTransportImpl transport = new MockTransportImpl();
        Connection connection = Proton.connection();
        transport.bind(connection);

        // Open the connection
        connection.open();

        // Create but don't open the session
        Session session = connection.session();

        // Open the link
        Link link = null;
        if(receiverLink)
        {
            link = session.receiver("myReceiver");
        }
        else
        {
            link = session.sender("mySender");
        }
        link.open();

        pumpMockTransport(transport);

        // Expect only an Open frame, no attach should be sent as the session isn't open
        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 1, transport.writes.size());
        assertTrue("Unexpected frame type", transport.writes.get(0) instanceof Open);

        // Now open the session, expect the Begin
        session.open();

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 2, transport.writes.size());
        assertTrue("Unexpected frame type", transport.writes.get(0) instanceof Open);
        assertTrue("Unexpected frame type", transport.writes.get(1) instanceof Begin);
        // Note: an Attach wasn't sent because link is no longer 'modified' after earlier pump. It
        // could easily be argued it should, given how the engine generally handles things. Seems
        // unlikely to be of much real world concern.
        //assertTrue("Unexpected frame type", transport.writes.get(2) instanceof Attach);
    }

    /*
     * Verify that no Attach frame is emitted by the Transport should a Receiver
     * be opened after the session End frame was sent.
     */
    @Test
    public void testReceiverAttachAfterEndSent()
    {
        doLinkAttachAfterEndSentTestImpl(true);
    }

    /*
     * Verify that no Attach frame is emitted by the Transport should a Sender
     * be opened after the session End frame was sent.
     */
    @Test
    public void testSenderAttachAfterEndSent()
    {
        doLinkAttachAfterEndSentTestImpl(false);
    }

    void doLinkAttachAfterEndSentTestImpl(boolean receiverLink)
    {
        MockTransportImpl transport = new MockTransportImpl();
        Connection connection = Proton.connection();
        transport.bind(connection);

        connection.open();

        Session session = connection.session();
        session.open();

        Link link = null;
        if(receiverLink)
        {
            link = session.receiver("myReceiver");
        }
        else
        {
            link = session.sender("mySender");
        }

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 2, transport.writes.size());

        assertTrue("Unexpected frame type", transport.writes.get(0) instanceof Open);
        assertTrue("Unexpected frame type", transport.writes.get(1) instanceof Begin);

        // Send the necessary responses to open/begin
        transport.handleFrame(new TransportFrame(0, new Open(), null));

        Begin begin = new Begin();
        begin.setRemoteChannel(UnsignedShort.valueOf((short) 0));
        transport.handleFrame(new TransportFrame(0, begin, null));

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 2, transport.writes.size());

        // Cause a End frame to be sent
        session.close();
        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 3, transport.writes.size());
        assertTrue("Unexpected frame type", transport.writes.get(2) instanceof End);

        // Open the link and verify the transport doesn't
        // send any Attach frame, as an End frame was sent already.
        link.open();
        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 3, transport.writes.size());
    }

    /*
     * Verify that no Attach frame is emitted by the Transport should a Receiver
     * be closed after the session End frame was sent.
     */
    @Test
    public void testReceiverCloseAfterEndSent()
    {
        doLinkDetachAfterEndSentTestImpl(true);
    }

    /*
     * Verify that no Attach frame is emitted by the Transport should a Sender
     * be closed after the session End frame was sent.
     */
    @Test
    public void testSenderCloseAfterEndSent()
    {
        doLinkDetachAfterEndSentTestImpl(false);
    }

    void doLinkDetachAfterEndSentTestImpl(boolean receiverLink)
    {
        MockTransportImpl transport = new MockTransportImpl();
        Connection connection = Proton.connection();
        transport.bind(connection);

        connection.open();

        Session session = connection.session();
        session.open();

        Link link = null;
        if(receiverLink)
        {
            link = session.receiver("myReceiver");
        }
        else
        {
            link = session.sender("mySender");
        }
        link.open();

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 3, transport.writes.size());

        assertTrue("Unexpected frame type", transport.writes.get(0) instanceof Open);
        assertTrue("Unexpected frame type", transport.writes.get(1) instanceof Begin);
        assertTrue("Unexpected frame type", transport.writes.get(2) instanceof Attach);

        // Send the necessary responses to open/begin
        transport.handleFrame(new TransportFrame(0, new Open(), null));

        Begin begin = new Begin();
        begin.setRemoteChannel(UnsignedShort.valueOf((short) 0));
        transport.handleFrame(new TransportFrame(0, begin, null));

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 3, transport.writes.size());

        // Cause an End frame to be sent
        session.close();
        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 4, transport.writes.size());
        assertTrue("Unexpected frame type", transport.writes.get(3) instanceof End);

        // Close the link and verify the transport doesn't
        // send any Detach frame, as an End frame was sent already.
        link.close();
        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 4, transport.writes.size());
    }

    /*
     * No frames should be written until the Connection object is
     * opened, at which point the Open and Begin frames should
     * be pipelined together.
     */
    @Test
    public void testReceiverFlowBeforeOpenConnection()
    {
        MockTransportImpl transport = new MockTransportImpl();
        Connection connection = Proton.connection();
        transport.bind(connection);

        Session session = connection.session();
        session.open();

        Receiver reciever = session.receiver("myReceiver");
        reciever.flow(5);

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 0, transport.writes.size());

        // Now open the connection, expect the Open and Begin frames but
        // nothing else as we haven't opened the receiver itself yet.
        connection.open();

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 2, transport.writes.size());

        assertTrue("Unexpected frame type", transport.writes.get(0) instanceof Open);
        assertTrue("Unexpected frame type", transport.writes.get(1) instanceof Begin);
    }

    @Test
    public void testSenderSendBeforeOpenConnection()
    {
        MockTransportImpl transport = new MockTransportImpl();

        Connection connection = Proton.connection();
        transport.bind(connection);

        Collector collector = Collector.Factory.create();
        connection.collect(collector);

        Session session = connection.session();
        session.open();

        String linkName = "mySender";
        Sender sender = session.sender(linkName);
        sender.open();

        sendMessage(sender, "tag1", "content1");

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 0, transport.writes.size());

        // Now open the connection, expect the Open and Begin and Attach frames but
        // nothing else as we the sender wont have credit yet.
        connection.open();

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 3, transport.writes.size());

        assertTrue("Unexpected frame type", transport.writes.get(0) instanceof Open);
        assertTrue("Unexpected frame type", transport.writes.get(1) instanceof Begin);
        assertTrue("Unexpected frame type", transport.writes.get(2) instanceof Attach);

        // Send the necessary responses to open/begin/attach then give sender credit
        transport.handleFrame(new TransportFrame(0, new Open(), null));

        Begin begin = new Begin();
        begin.setRemoteChannel(UnsignedShort.valueOf((short) 0));
        transport.handleFrame(new TransportFrame(0, begin, null));

        Attach attach = new Attach();
        attach.setHandle(UnsignedInteger.ZERO);
        attach.setRole(Role.RECEIVER);
        attach.setName(linkName);
        attach.setInitialDeliveryCount(UnsignedInteger.ZERO);
        transport.handleFrame(new TransportFrame(0, attach, null));

        Flow flow = new Flow();
        flow.setHandle(UnsignedInteger.ZERO);
        flow.setDeliveryCount(UnsignedInteger.ZERO);
        flow.setNextIncomingId(UnsignedInteger.ONE);
        flow.setNextOutgoingId(UnsignedInteger.ZERO);
        flow.setIncomingWindow(UnsignedInteger.valueOf(1024));
        flow.setOutgoingWindow(UnsignedInteger.valueOf(1024));
        flow.setLinkCredit(UnsignedInteger.valueOf(10));

        transport.handleFrame(new TransportFrame(0, flow, null));

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 3, transport.writes.size());

        // Now pump the transport again and expect a transfer for the message
        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 4, transport.writes.size());
        assertTrue("Unexpected frame type", transport.writes.get(3) instanceof Transfer);
    }

    @Test
    public void testEmitFlowEventOnSend()
    {
        doEmitFlowOnSendTestImpl(true);
    }

    public void testSupressFlowEventOnSend()
    {
        doEmitFlowOnSendTestImpl(false);
    }

    void doEmitFlowOnSendTestImpl(boolean emitFlowEventOnSend)
    {
        MockTransportImpl transport = new MockTransportImpl();
        transport.setEmitFlowEventOnSend(emitFlowEventOnSend);

        Connection connection = Proton.connection();
        transport.bind(connection);

        Collector collector = Collector.Factory.create();
        connection.collect(collector);

        Session session = connection.session();
        session.open();

        String linkName = "mySender";
        Sender sender = session.sender(linkName);
        sender.open();

        sendMessage(sender, "tag1", "content1");

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 0, transport.writes.size());

        assertEvents(collector, Event.Type.CONNECTION_INIT, Event.Type.SESSION_INIT, Event.Type.SESSION_LOCAL_OPEN,
                                Event.Type.TRANSPORT, Event.Type.LINK_INIT, Event.Type.LINK_LOCAL_OPEN, Event.Type.TRANSPORT);

        // Now open the connection, expect the Open and Begin frames but
        // nothing else as we haven't opened the receiver itself yet.
        connection.open();

        pumpMockTransport(transport);

        assertEvents(collector, Event.Type.CONNECTION_LOCAL_OPEN, Event.Type.TRANSPORT);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 3, transport.writes.size());

        assertTrue("Unexpected frame type", transport.writes.get(0) instanceof Open);
        assertTrue("Unexpected frame type", transport.writes.get(1) instanceof Begin);
        assertTrue("Unexpected frame type", transport.writes.get(2) instanceof Attach);

        // Send the necessary responses to open/begin/attach then give sender credit
        transport.handleFrame(new TransportFrame(0, new Open(), null));

        Begin begin = new Begin();
        begin.setRemoteChannel(UnsignedShort.valueOf((short) 0));
        transport.handleFrame(new TransportFrame(0, begin, null));

        Attach attach = new Attach();
        attach.setHandle(UnsignedInteger.ZERO);
        attach.setRole(Role.RECEIVER);
        attach.setName(linkName);
        attach.setInitialDeliveryCount(UnsignedInteger.ZERO);
        transport.handleFrame(new TransportFrame(0, attach, null));

        Flow flow = new Flow();
        flow.setHandle(UnsignedInteger.ZERO);
        flow.setDeliveryCount(UnsignedInteger.ZERO);
        flow.setNextIncomingId(UnsignedInteger.ONE);
        flow.setNextOutgoingId(UnsignedInteger.ZERO);
        flow.setIncomingWindow(UnsignedInteger.valueOf(1024));
        flow.setOutgoingWindow(UnsignedInteger.valueOf(1024));
        flow.setLinkCredit(UnsignedInteger.valueOf(10));

        transport.handleFrame(new TransportFrame(0, flow, null));

        assertEvents(collector, Event.Type.CONNECTION_REMOTE_OPEN, Event.Type.SESSION_REMOTE_OPEN,
                                Event.Type.LINK_REMOTE_OPEN, Event.Type.LINK_FLOW);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 3, transport.writes.size());

        // Now pump the transport again and expect a transfer for the message
        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 4, transport.writes.size());
        assertTrue("Unexpected frame type", transport.writes.get(3) instanceof Transfer);

        // Verify that we did, or did not, emit a flow event
        if(emitFlowEventOnSend)
        {
            assertEvents(collector, Event.Type.LINK_FLOW);
        }
        else
        {
            assertNoEvents(collector);
        }
    }

    /**
     * Verify that no Begin frame is emitted by the Transport should a Session open
     * after the Close frame was sent.
     */
    @Test
    public void testSessionBeginAfterCloseSent()
    {
        MockTransportImpl transport = new MockTransportImpl();
        Connection connection = Proton.connection();
        transport.bind(connection);

        connection.open();

        Session session = connection.session();

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 1, transport.writes.size());

        assertTrue("Unexpected frame type", transport.writes.get(0) instanceof Open);

        // Send the necessary response to Open
        transport.handleFrame(new TransportFrame(0, new Open(), null));

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 1, transport.writes.size());

        // Cause a Close frame to be sent
        connection.close();
        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 2, transport.writes.size());
        assertTrue("Unexpected frame type", transport.writes.get(1) instanceof Close);

        // Open the session and verify the transport doesn't
        // send any Begin frame, as a Close frame was sent already.
        session.open();
        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 2, transport.writes.size());
    }

    /**
     * Verify that no End frame is emitted by the Transport should a Session close
     * after the Close frame was sent.
     */
    @Test
    public void testSessionEndAfterCloseSent()
    {
        MockTransportImpl transport = new MockTransportImpl();
        Connection connection = Proton.connection();
        transport.bind(connection);

        connection.open();

        Session session = connection.session();
        session.open();

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 2, transport.writes.size());

        assertTrue("Unexpected frame type", transport.writes.get(0) instanceof Open);
        assertTrue("Unexpected frame type", transport.writes.get(1) instanceof Begin);

        // Send the necessary responses to open/begin
        transport.handleFrame(new TransportFrame(0, new Open(), null));

        Begin begin = new Begin();
        begin.setRemoteChannel(UnsignedShort.valueOf((short) 0));
        transport.handleFrame(new TransportFrame(0, begin, null));

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 2, transport.writes.size());

        // Cause a Close frame to be sent
        connection.close();
        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 3, transport.writes.size());
        assertTrue("Unexpected frame type", transport.writes.get(2) instanceof Close);

        // Close the session and verify the transport doesn't
        // send any End frame, as a Close frame was sent already.
        session.close();
        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 3, transport.writes.size());
    }

    @Test
    public void testEmittedSessionIncomingWindow()
    {
        doSessionIncomingWindowTestImpl(false, false);
        doSessionIncomingWindowTestImpl(true, false);
        doSessionIncomingWindowTestImpl(false, true);
        doSessionIncomingWindowTestImpl(true, true);
    }

    private void doSessionIncomingWindowTestImpl(boolean setFrameSize, boolean setSessionCapacity) {
        MockTransportImpl transport;
        if(setFrameSize) {
            transport = new MockTransportImpl(5*1024);
        } else {
            transport = new MockTransportImpl();
        }

        Connection connection = Proton.connection();
        transport.bind(connection);

        connection.open();

        Session session = connection.session();
        int sessionCapacity = 0;
        if(setSessionCapacity) {
            sessionCapacity = 100*1024;
            session.setIncomingCapacity(sessionCapacity);
        }

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 1, transport.writes.size());
        assertTrue("Unexpected frame type", transport.writes.get(0) instanceof Open);
        // Provide an Open response
        transport.handleFrame(new TransportFrame(0, new Open(), null));

        // Open session and verify emitted incoming window
        session.open();
        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 2, transport.writes.size());
        assertTrue("Unexpected frame type", transport.writes.get(1) instanceof Begin);
        Begin sentBegin = (Begin) transport.writes.get(1);

        assertEquals("Unexpected session capacity", sessionCapacity, session.getIncomingCapacity());

        int expectedWindowSize = 2147483647;
        if(setSessionCapacity && setFrameSize) {
            expectedWindowSize = (100*1024) / (5*1024); // capacity / frameSize
        }

        assertEquals("Unexpected session window", UnsignedInteger.valueOf(expectedWindowSize), sentBegin.getIncomingWindow());

        // Open receiver
        String linkName = "myReceiver";
        Receiver receiver = session.receiver(linkName);
        receiver.open();

        pumpMockTransport(transport);

        assertTrue("Unexpected frame type", transport.writes.get(2) instanceof Attach);
        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 3, transport.writes.size());

        // Provide an begin+attach response
        Begin beginResponse = new Begin();
        beginResponse.setRemoteChannel(UnsignedShort.valueOf((short) 0));
        beginResponse.setNextOutgoingId(UnsignedInteger.ONE);
        beginResponse.setIncomingWindow(UnsignedInteger.valueOf(1024));
        beginResponse.setOutgoingWindow(UnsignedInteger.valueOf(1024));
        transport.handleFrame(new TransportFrame(0, beginResponse, null));

        Attach attach = new Attach();
        attach.setHandle(UnsignedInteger.ZERO);
        attach.setRole(Role.SENDER);
        attach.setName(linkName);
        attach.setInitialDeliveryCount(UnsignedInteger.ZERO);
        transport.handleFrame(new TransportFrame(0, attach, null));

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 3, transport.writes.size());

        // Flow some credit, verify emitted incoming window remains the same
        receiver.flow(1);

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 4, transport.writes.size());
        assertTrue("Unexpected frame type", transport.writes.get(3) instanceof Flow);
        Flow sentFlow = (Flow) transport.writes.get(3);

        assertEquals("Unexpected session window", UnsignedInteger.valueOf(expectedWindowSize), sentFlow.getIncomingWindow());

        // Provide a transfer, don't consume it, flow more credit, verify the emitted
        // incoming window (should reduce 1 if capacity and frame size set)
        String deliveryTag = "tag1";
        String messageContent = "content1";
        handleTransfer(transport, 1, deliveryTag, messageContent);

        assertTrue("Unexpected session byte count", session.getIncomingBytes() > 0);

        receiver.flow(1);

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 5, transport.writes.size());
        assertTrue("Unexpected frame type", transport.writes.get(4) instanceof Flow);
        sentFlow = (Flow) transport.writes.get(4);

        if(setSessionCapacity && setFrameSize) {
            expectedWindowSize = expectedWindowSize -1;
        }
        assertEquals("Unexpected session window", UnsignedInteger.valueOf(expectedWindowSize), sentFlow.getIncomingWindow());

        // Consume the transfer then flow more credit, verify the emitted
        // incoming window (should increase 1 if capacity and frame size set)
        verifyDelivery(receiver, deliveryTag, messageContent);
        assertEquals("Unexpected session byte count", 0, session.getIncomingBytes());

        receiver.flow(1);

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 6, transport.writes.size());
        assertTrue("Unexpected frame type", transport.writes.get(5) instanceof Flow);
        sentFlow = (Flow) transport.writes.get(5);

        if(setSessionCapacity && setFrameSize) {
            expectedWindowSize = expectedWindowSize +1;
        }
        assertEquals("Unexpected session window", UnsignedInteger.valueOf(expectedWindowSize), sentFlow.getIncomingWindow());
    }

    /**
     * Verify that no Attach frame is emitted by the Transport should a Receiver
     * be opened after the Close frame was sent.
     */
    @Test
    public void testReceiverAttachAfterCloseSent()
    {
        doLinkAttachAfterCloseSentTestImpl(true);
    }

    /**
     * Verify that no Attach frame is emitted by the Transport should a Sender
     * be opened after the Close frame was sent.
     */
    @Test
    public void testSenderAttachAfterCloseSent()
    {
        doLinkAttachAfterCloseSentTestImpl(false);
    }

    void doLinkAttachAfterCloseSentTestImpl(boolean receiverLink)
    {
        MockTransportImpl transport = new MockTransportImpl();
        Connection connection = Proton.connection();
        transport.bind(connection);

        connection.open();

        Session session = connection.session();
        session.open();

        Link link = null;
        if(receiverLink)
        {
            link = session.receiver("myReceiver");
        }
        else
        {
            link = session.sender("mySender");
        }

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 2, transport.writes.size());

        assertTrue("Unexpected frame type", transport.writes.get(0) instanceof Open);
        assertTrue("Unexpected frame type", transport.writes.get(1) instanceof Begin);

        // Send the necessary responses to open/begin
        transport.handleFrame(new TransportFrame(0, new Open(), null));

        Begin begin = new Begin();
        begin.setRemoteChannel(UnsignedShort.valueOf((short) 0));
        transport.handleFrame(new TransportFrame(0, begin, null));

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 2, transport.writes.size());

        // Cause a Close frame to be sent
        connection.close();
        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 3, transport.writes.size());
        assertTrue("Unexpected frame type", transport.writes.get(2) instanceof Close);

        // Open the link and verify the transport doesn't
        // send any Attach frame, as a Close frame was sent already.
        link.open();
        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 3, transport.writes.size());
    }

    /**
     * Verify that no Flow frame is emitted by the Transport should a Receiver
     * have credit added after the Close frame was sent.
     */
    @Test
    public void testReceiverFlowAfterCloseSent()
    {
        MockTransportImpl transport = new MockTransportImpl();
        Connection connection = Proton.connection();
        transport.bind(connection);

        connection.open();

        Session session = connection.session();
        session.open();

        String linkName = "myReceiver";
        Receiver receiver = session.receiver(linkName);
        receiver.open();

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 3, transport.writes.size());

        assertTrue("Unexpected frame type", transport.writes.get(0) instanceof Open);
        assertTrue("Unexpected frame type", transport.writes.get(1) instanceof Begin);
        assertTrue("Unexpected frame type", transport.writes.get(2) instanceof Attach);

        // Send the necessary responses to open/begin/attach
        transport.handleFrame(new TransportFrame(0, new Open(), null));

        Begin begin = new Begin();
        begin.setRemoteChannel(UnsignedShort.valueOf((short) 0));
        transport.handleFrame(new TransportFrame(0, begin, null));

        Attach attach = new Attach();
        attach.setHandle(UnsignedInteger.ZERO);
        attach.setRole(Role.RECEIVER);
        attach.setName(linkName);
        attach.setInitialDeliveryCount(UnsignedInteger.ZERO);
        transport.handleFrame(new TransportFrame(0, attach, null));

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 3, transport.writes.size());

        // Cause the Close frame to be sent
        connection.close();
        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 4, transport.writes.size());
        assertTrue("Unexpected frame type", transport.writes.get(3) instanceof Close);

        // Grant new credit for the Receiver and verify the transport doesn't
        // send any Flow frame, as a Close frame was sent already.
        receiver.flow(1);
        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 4, transport.writes.size());
    }

    /**
     * Verify that no Flow frame is emitted by the Transport should a Receiver
     * have pending drain when a detach is sent for that receiver.
     */
    @Test
    public void testNoReceiverFlowAfterDetachSentWhileDraining()
    {
        MockTransportImpl transport = new MockTransportImpl();
        Connection connection = Proton.connection();
        transport.bind(connection);

        connection.open();

        Session session = connection.session();
        session.open();

        String linkName = "myReceiver";
        Receiver receiver = session.receiver(linkName);
        receiver.open();

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 3, transport.writes.size());

        assertTrue("Unexpected frame type", transport.writes.get(0) instanceof Open);
        assertTrue("Unexpected frame type", transport.writes.get(1) instanceof Begin);
        assertTrue("Unexpected frame type", transport.writes.get(2) instanceof Attach);

        // Send the necessary responses to open/begin/attach
        transport.handleFrame(new TransportFrame(0, new Open(), null));

        Begin begin = new Begin();
        begin.setRemoteChannel(UnsignedShort.valueOf((short) 0));
        transport.handleFrame(new TransportFrame(0, begin, null));

        Attach attach = new Attach();
        attach.setHandle(UnsignedInteger.ZERO);
        attach.setRole(Role.RECEIVER);
        attach.setName(linkName);
        attach.setInitialDeliveryCount(UnsignedInteger.ZERO);
        transport.handleFrame(new TransportFrame(0, attach, null));

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 3, transport.writes.size());

        // Start a drain for the Receiver and verify the transport doesn't
        // send any Flow frame, due to the detach being initiated.
        receiver.drain(10);
        pumpMockTransport(transport);

        // Cause the Detach frame to be sent
        receiver.detach();
        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 5, transport.writes.size());
        assertTrue("Unexpected frame type", transport.writes.get(4) instanceof Detach);
    }

    /**
     * Verify that no Flow frame is emitted by the Transport should a Sender
     * have credit drained added after the Close frame was sent.
     */
    @Test
    public void testSenderFlowAfterCloseSent()
    {
        MockTransportImpl transport = new MockTransportImpl();

        Connection connection = Proton.connection();
        transport.bind(connection);

        connection.open();

        Collector collector = Collector.Factory.create();
        connection.collect(collector);

        Session session = connection.session();
        session.open();

        String linkName = "mySender";
        Sender sender = session.sender(linkName);
        sender.open();

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 3, transport.writes.size());

        assertTrue("Unexpected frame type", transport.writes.get(0) instanceof Open);
        assertTrue("Unexpected frame type", transport.writes.get(1) instanceof Begin);
        assertTrue("Unexpected frame type", transport.writes.get(2) instanceof Attach);

        assertFalse("Should not be in drain yet", sender.getDrain());

        // Send the necessary responses to open/begin/attach then give sender credit and drain
        transport.handleFrame(new TransportFrame(0, new Open(), null));

        Begin begin = new Begin();
        begin.setRemoteChannel(UnsignedShort.valueOf((short) 0));
        transport.handleFrame(new TransportFrame(0, begin, null));

        Attach attach = new Attach();
        attach.setHandle(UnsignedInteger.ZERO);
        attach.setRole(Role.RECEIVER);
        attach.setName(linkName);
        attach.setInitialDeliveryCount(UnsignedInteger.ZERO);
        transport.handleFrame(new TransportFrame(0, attach, null));

        int credit = 10;
        Flow flow = new Flow();
        flow.setHandle(UnsignedInteger.ZERO);
        flow.setDeliveryCount(UnsignedInteger.ZERO);
        flow.setNextIncomingId(UnsignedInteger.ONE);
        flow.setNextOutgoingId(UnsignedInteger.ZERO);
        flow.setIncomingWindow(UnsignedInteger.valueOf(1024));
        flow.setOutgoingWindow(UnsignedInteger.valueOf(1024));
        flow.setDrain(true);
        flow.setLinkCredit(UnsignedInteger.valueOf(credit));

        transport.handleFrame(new TransportFrame(0, flow, null));

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 3, transport.writes.size());

        assertTrue("Should not be in drain", sender.getDrain());
        assertEquals("Should have credit", credit, sender.getCredit());

        // Cause the Close frame to be sent
        connection.close();
        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 4, transport.writes.size());
        assertTrue("Unexpected frame type", transport.writes.get(3) instanceof Close);

        // Drain the credit and verify the transport doesn't
        // send any Flow frame, as a Close frame was sent already.
        int drained = sender.drained();
        assertEquals("Should have drained all credit", credit, drained);

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 4, transport.writes.size());
    }

    /**
     * Verify that no Disposition frame is emitted by the Transport should a Delivery
     * have disposition applied after the Close frame was sent.
     */
    @Test
    public void testDispositionAfterCloseSent()
    {
        MockTransportImpl transport = new MockTransportImpl();
        Connection connection = Proton.connection();
        transport.bind(connection);

        connection.open();

        Session session = connection.session();
        session.open();

        String linkName = "myReceiver";
        Receiver receiver = session.receiver(linkName);
        receiver.flow(5);
        receiver.open();

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 4, transport.writes.size());

        assertTrue("Unexpected frame type", transport.writes.get(0) instanceof Open);
        assertTrue("Unexpected frame type", transport.writes.get(1) instanceof Begin);
        assertTrue("Unexpected frame type", transport.writes.get(2) instanceof Attach);
        assertTrue("Unexpected frame type", transport.writes.get(3) instanceof Flow);

        Delivery delivery = receiver.current();
        assertNull("Should not yet have a delivery", delivery);

        // Send the necessary responses to open/begin/attach as well as a transfer
        transport.handleFrame(new TransportFrame(0, new Open(), null));

        Begin begin = new Begin();
        begin.setRemoteChannel(UnsignedShort.valueOf((short) 0));
        begin.setNextOutgoingId(UnsignedInteger.ONE);
        begin.setIncomingWindow(UnsignedInteger.valueOf(1024));
        begin.setOutgoingWindow(UnsignedInteger.valueOf(1024));
        transport.handleFrame(new TransportFrame(0, begin, null));

        Attach attach = new Attach();
        attach.setHandle(UnsignedInteger.ZERO);
        attach.setRole(Role.SENDER);
        attach.setName(linkName);
        attach.setInitialDeliveryCount(UnsignedInteger.ZERO);
        transport.handleFrame(new TransportFrame(0, attach, null));

        String deliveryTag = "tag1";
        String messageContent = "content1";
        handleTransfer(transport, 1, deliveryTag, messageContent);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 4, transport.writes.size());

        delivery = verifyDelivery(receiver, deliveryTag, messageContent);
        assertNotNull("Should now have a delivery", delivery);

        // Cause the Close frame to be sent
        connection.close();
        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 5, transport.writes.size());
        assertTrue("Unexpected frame type", transport.writes.get(4) instanceof Close);

        delivery.disposition(Released.getInstance());
        delivery.settle();

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 5, transport.writes.size());
    }

    /**
     * Verify that no Transfer frame is emitted by the Transport should a Delivery
     * be sendable after the Close frame was sent.
     */
    @Test
    public void testTransferAfterCloseSent()
    {
        MockTransportImpl transport = new MockTransportImpl();

        Connection connection = Proton.connection();
        transport.bind(connection);

        connection.open();

        Collector collector = Collector.Factory.create();
        connection.collect(collector);

        Session session = connection.session();
        session.open();

        String linkName = "mySender";
        Sender sender = session.sender(linkName);
        sender.open();

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 3, transport.writes.size());

        assertTrue("Unexpected frame type", transport.writes.get(0) instanceof Open);
        assertTrue("Unexpected frame type", transport.writes.get(1) instanceof Begin);
        assertTrue("Unexpected frame type", transport.writes.get(2) instanceof Attach);

        // Send the necessary responses to open/begin/attach then give sender credit
        transport.handleFrame(new TransportFrame(0, new Open(), null));

        Begin begin = new Begin();
        begin.setRemoteChannel(UnsignedShort.valueOf((short) 0));
        transport.handleFrame(new TransportFrame(0, begin, null));

        Attach attach = new Attach();
        attach.setHandle(UnsignedInteger.ZERO);
        attach.setRole(Role.RECEIVER);
        attach.setName(linkName);
        attach.setInitialDeliveryCount(UnsignedInteger.ZERO);
        transport.handleFrame(new TransportFrame(0, attach, null));

        Flow flow = new Flow();
        flow.setHandle(UnsignedInteger.ZERO);
        flow.setDeliveryCount(UnsignedInteger.ZERO);
        flow.setNextIncomingId(UnsignedInteger.ONE);
        flow.setNextOutgoingId(UnsignedInteger.ZERO);
        flow.setIncomingWindow(UnsignedInteger.valueOf(1024));
        flow.setOutgoingWindow(UnsignedInteger.valueOf(1024));
        flow.setLinkCredit(UnsignedInteger.valueOf(10));

        transport.handleFrame(new TransportFrame(0, flow, null));

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 3, transport.writes.size());

        // Cause the Close frame to be sent
        connection.close();
        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 4, transport.writes.size());
        assertTrue("Unexpected frame type", transport.writes.get(3) instanceof Close);

        // Send a new message and verify the transport doesn't
        // send any Transfer frame, as a Close frame was sent already.
        sendMessage(sender, "tag1", "content1");
        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 4, transport.writes.size());
    }

    private void assertNoEvents(Collector collector)
    {
        assertEvents(collector);
    }

    private void assertEvents(Collector collector, Event.Type... expectedEventTypes)
    {

        if(expectedEventTypes.length == 0)
        {
            assertNull("Expected no events, but at least one was present: " + collector.peek(), collector.peek());
        }
        else
        {
            ArrayList<Event.Type> eventTypesList = new ArrayList<Event.Type>();
            Event event = null;
            while ((event = collector.peek()) != null) {
                eventTypesList.add(event.getType());
                collector.pop();
            }

            assertArrayEquals("Unexpected event types: " + eventTypesList, expectedEventTypes, eventTypesList.toArray(new Event.Type[0]));
        }
    }

    private void pumpMockTransport(MockTransportImpl transport)
    {
        while(transport.pending() > 0)
        {
            transport.pop(transport.head().remaining());
        }
    }

    private String getFrameTypesWritten(MockTransportImpl transport)
    {
        String result = "";
        for(FrameBody f : transport.writes) {
            result += f.getClass().getSimpleName();
            result += ",";
        }

        if(result.isEmpty()) {
            return "no-frames-written";
        } else {
            return result;
        }
    }

    private Delivery sendMessage(Sender sender, String deliveryTag, String messageContent)
    {
        byte[] tag = deliveryTag.getBytes(StandardCharsets.UTF_8);

        Message m = Message.Factory.create();
        m.setBody(new AmqpValue(messageContent));

        byte[] encoded = new byte[BUFFER_SIZE];
        int len = m.encode(encoded, 0, BUFFER_SIZE);

        assertTrue("given array was too small", len < BUFFER_SIZE);

        Delivery delivery = sender.delivery(tag);

        int sent = sender.send(encoded, 0, len);

        assertEquals("sender unable to send all data at once as assumed for simplicity", len, sent);

        boolean senderAdvanced = sender.advance();
        assertTrue("sender has not advanced", senderAdvanced);

        return delivery;
    }

    private void handleTransfer(TransportImpl transport, int deliveryNumber, String deliveryTag, String messageContent)
    {
        byte[] tag = deliveryTag.getBytes(StandardCharsets.UTF_8);

        Message m = Message.Factory.create();
        m.setBody(new AmqpValue(messageContent));

        byte[] encoded = new byte[BUFFER_SIZE];
        int len = m.encode(encoded, 0, BUFFER_SIZE);

        assertTrue("given array was too small", len < BUFFER_SIZE);

        Transfer transfer = new Transfer();
        transfer.setDeliveryId(UnsignedInteger.valueOf(deliveryNumber));
        transfer.setHandle(UnsignedInteger.ZERO);
        transfer.setDeliveryTag(new Binary(tag));
        transfer.setMessageFormat(UnsignedInteger.valueOf(DeliveryImpl.DEFAULT_MESSAGE_FORMAT));

        transport.handleFrame(new TransportFrame(0, transfer, new Binary(encoded, 0, len)));
    }

    private Delivery verifyDelivery(Receiver receiver, String deliveryTag, String messageContent)
    {
        Delivery delivery = receiver.current();

        assertTrue(Arrays.equals(deliveryTag.getBytes(StandardCharsets.UTF_8), delivery.getTag()));

        assertNull(delivery.getLocalState());
        assertNull(delivery.getRemoteState());

        assertFalse(delivery.isPartial());
        assertTrue(delivery.isReadable());

        byte[] received = new byte[BUFFER_SIZE];
        int len = receiver.recv(received, 0, BUFFER_SIZE);

        assertTrue("given array was too small", len < BUFFER_SIZE);

        Message m = Proton.message();
        m.decode(received, 0, len);

        Object messageBody = ((AmqpValue)m.getBody()).getValue();
        assertEquals("Unexpected message content", messageContent, messageBody);

        boolean receiverAdvanced = receiver.advance();
        assertTrue("receiver has not advanced", receiverAdvanced);

        return delivery;
    }

    private Delivery verifyDeliveryRawPayload(Receiver receiver, String deliveryTag, byte[] payload)
    {
        Delivery delivery = receiver.current();

        assertTrue(Arrays.equals(deliveryTag.getBytes(StandardCharsets.UTF_8), delivery.getTag()));

        assertFalse(delivery.isPartial());
        assertTrue(delivery.isReadable());

        byte[] received = new byte[delivery.pending()];
        int len = receiver.recv(received, 0, BUFFER_SIZE);

        assertEquals("unexpected length", len, received.length);

        assertArrayEquals("Received delivery payload not as expected", payload, received);

        boolean receiverAdvanced = receiver.advance();
        assertTrue("receiver has not advanced", receiverAdvanced);

        return delivery;
    }

    /**
     * Verify that the {@link TransportInternal#addTransportLayer(TransportLayer)} has the desired
     * effect by observing the wrapping effect on related transport input and output methods.
     */
    @Test
    public void testAddAdditionalTransportLayer()
    {
        Integer capacityOverride = 1957;
        Integer pendingOverride = 2846;

        MockTransportImpl transport = new MockTransportImpl();

        TransportWrapper mockWrapper = Mockito.mock(TransportWrapper.class);

        Mockito.when(mockWrapper.capacity()).thenReturn(capacityOverride);
        Mockito.when(mockWrapper.pending()).thenReturn(pendingOverride);

        TransportLayer mockLayer = Mockito.mock(TransportLayer.class);
        Mockito.when(mockLayer.wrap(Mockito.any(TransportInput.class), Mockito.any(TransportOutput.class))).thenReturn(mockWrapper);

        transport.addTransportLayer(mockLayer);

        assertEquals("Unexepcted value, layer override not effective", capacityOverride.intValue(), transport.capacity());
        assertEquals("Unexepcted value, layer override not effective", pendingOverride.intValue(), transport.pending());
    }

    @Test
    public void testAddAdditionalTransportLayerThrowsISEIfProcessingStarted()
    {
        MockTransportImpl transport = new MockTransportImpl();
        TransportLayer mockLayer = Mockito.mock(TransportLayer.class);

        transport.process();

        try
        {
            transport.addTransportLayer(mockLayer);
            fail("Expected exception to be thrown due to processing having started");
        }
        catch (IllegalStateException ise)
        {
            // expected
        }
    }

    @Test
    public void testEndpointOpenAndCloseAreIdempotent()
    {
        MockTransportImpl transport = new MockTransportImpl();

        Connection connection = Proton.connection();
        transport.bind(connection);

        Collector collector = Collector.Factory.create();
        connection.collect(collector);

        connection.open();
        connection.open();

        Session session = connection.session();
        session.open();

        String linkName = "mySender";
        Sender sender = session.sender(linkName);
        sender.open();

        pumpMockTransport(transport);

        assertEvents(collector, Event.Type.CONNECTION_INIT, Event.Type.CONNECTION_LOCAL_OPEN, Event.Type.TRANSPORT,
                                Event.Type.SESSION_INIT, Event.Type.SESSION_LOCAL_OPEN,
                                Event.Type.TRANSPORT, Event.Type.LINK_INIT, Event.Type.LINK_LOCAL_OPEN, Event.Type.TRANSPORT);

        pumpMockTransport(transport);

        connection.open();
        session.open();
        sender.open();

        assertNoEvents(collector);

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 3, transport.writes.size());

        assertTrue("Unexpected frame type", transport.writes.get(0) instanceof Open);
        assertTrue("Unexpected frame type", transport.writes.get(1) instanceof Begin);
        assertTrue("Unexpected frame type", transport.writes.get(2) instanceof Attach);

        // Send the necessary responses to open/begin/attach then give sender credit
        transport.handleFrame(new TransportFrame(0, new Open(), null));

        Begin begin = new Begin();
        begin.setRemoteChannel(UnsignedShort.valueOf((short) 0));
        transport.handleFrame(new TransportFrame(0, begin, null));

        Attach attach = new Attach();
        attach.setHandle(UnsignedInteger.ZERO);
        attach.setRole(Role.RECEIVER);
        attach.setName(linkName);
        attach.setInitialDeliveryCount(UnsignedInteger.ZERO);
        transport.handleFrame(new TransportFrame(0, attach, null));

        assertEvents(collector, Event.Type.CONNECTION_REMOTE_OPEN, Event.Type.SESSION_REMOTE_OPEN,
                                Event.Type.LINK_REMOTE_OPEN);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 3, transport.writes.size());

        // Now close the link and expect one event
        sender.close();
        sender.close();

        assertEvents(collector, Event.Type.LINK_LOCAL_CLOSE, Event.Type.TRANSPORT);

        pumpMockTransport(transport);

        sender.close();

        assertNoEvents(collector);

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 4, transport.writes.size());
        assertTrue("Unexpected frame type", transport.writes.get(3) instanceof Detach);
    }

    @Test
    public void testInitialRemoteMaxFrameSizeOverride()
    {
        MockTransportImpl transport = new MockTransportImpl();
        transport.setInitialRemoteMaxFrameSize(768);

        assertEquals("Unexpected value : " + getFrameTypesWritten(transport), 768, transport.getRemoteMaxFrameSize());

        Connection connection = Proton.connection();
        transport.bind(connection);
        connection.open();
        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 1, transport.writes.size());
        assertTrue("Unexpected frame type", transport.writes.get(0) instanceof Open);

        try
        {
            transport.setInitialRemoteMaxFrameSize(12345);
            fail("expected an exception");
        }
        catch (IllegalStateException ise )
        {
            //expected
        }

        // Send the necessary responses to open
        Open open = new Open();
        open.setMaxFrameSize(UnsignedInteger.valueOf(4567));
        transport.handleFrame(new TransportFrame(0, open, null));

        assertEquals("Unexpected value : " + getFrameTypesWritten(transport), 4567, transport.getRemoteMaxFrameSize());
    }

    @Test
    public void testTickWithZeroIdleTimeoutsGivesZeroDeadline()
    {
        doTickWithNoIdleTimeoutGivesZeroDeadlineTestImpl(true);
    }

    @Test
    public void testTickWithNullIdleTimeoutsGivesZeroDeadline()
    {
        doTickWithNoIdleTimeoutGivesZeroDeadlineTestImpl(false);
    }

    private void doTickWithNoIdleTimeoutGivesZeroDeadlineTestImpl(boolean useZero) {
        MockTransportImpl transport = new MockTransportImpl();
        Connection connection = Proton.connection();
        transport.bind(connection);

        connection.open();
        while(transport.pending() > 0) {
            transport.pop(transport.head().remaining());
        }

        assertEquals("should have written data", 1, transport.writes.size());
        FrameBody sentOpenFrame = transport.writes.get(0);
        assertNotNull("should have written a non-empty frame", sentOpenFrame);
        assertTrue("should have written an open frame", sentOpenFrame instanceof Open);
        assertNull("should not have had an idletimeout value", ((Open)sentOpenFrame).getIdleTimeOut());

        // Handle the peer transmitting their open with null/zero timeout.
        Open open = new Open();
        if(useZero) {
            open.setIdleTimeOut(UnsignedInteger.ZERO);
        } else {
            open.setIdleTimeOut(null);
        }
        TransportFrame openFrame = new TransportFrame(CHANNEL_ID, open, null);
        transport.handleFrame(openFrame);
        pumpMockTransport(transport);

        long deadline = transport.tick(0);
        assertEquals("Unexpected deadline returned", 0, deadline);

        deadline = transport.tick(10);
        assertEquals("Unexpected deadline returned", 0, deadline);
    }

    @Test
    public void testTickWithLocalTimeout()
    {
        // all-positive
        doTickWithLocalTimeoutTestImpl(4000, 10000, 14000, 18000, 22000);

        // all-negative
        doTickWithLocalTimeoutTestImpl(2000, -100000, -98000, -96000, -94000);

        // negative to positive missing 0
        doTickWithLocalTimeoutTestImpl(500, -950, -450, 50, 550);

        // negative to positive striking 0
        doTickWithLocalTimeoutTestImpl(3000, -6000, -3000, 1, 3001);
    }

    private void doTickWithLocalTimeoutTestImpl(int localTimeout, long tick1, long expectedDeadline1, long expectedDeadline2, long expectedDeadline3)
    {
        MockTransportImpl transport = new MockTransportImpl();
        Connection connection = Proton.connection();
        transport.bind(connection);

        // Set our local idleTimeout
        transport.setIdleTimeout(localTimeout);

        connection.open();
        pumpMockTransport(transport);

        assertEquals("should have written data", 1, transport.writes.size());
        Object sentOpenFrame = transport.writes.get(0);
        assertNotNull("should have written a non-empty frame", sentOpenFrame);
        assertTrue("should have written an open frame", sentOpenFrame instanceof Open);
        assertEquals("should have had an idletimeout value half our actual timeout", UnsignedInteger.valueOf(localTimeout / 2), ((Open)sentOpenFrame).getIdleTimeOut());

        // Receive Protocol header
        processInput(transport, ByteBuffer.wrap(new byte[] {'A', 'M', 'Q', 'P', 0x00, 0x01, 0x00, 0x00}));

        // Handle the peer transmitting their open, without timeout.
        Open open = new Open();
        open.setIdleTimeOut(null);
        TransportFrame openFrame = new TransportFrame(CHANNEL_ID, open, null);
        transport.handleFrame(openFrame);
        pumpMockTransport(transport);

        long deadline = transport.tick(tick1);
        assertEquals("Unexpected deadline returned", expectedDeadline1, deadline);

        // Wait for less time than the deadline with no data - get the same value
        long interimTick = tick1 + 10;
        assertTrue (interimTick < expectedDeadline1);
        assertEquals("When the deadline hasn't been reached tick() should return the previous deadline",  expectedDeadline1, transport.tick(interimTick));
        assertEquals("When the deadline hasn't been reached tick() shouldn't write data", 1, transport.writes.size());

        // Receive Empty frame to satisfy local deadline
        processInput(transport,  ByteBuffer.wrap(new byte[] {0x00, 0x00, 0x00, 0x08, 0x02, 0x00, 0x00, 0x00}));

        deadline = transport.tick(expectedDeadline1);
        assertEquals("When the deadline has been reached expected a new local deadline to be returned", expectedDeadline2, deadline);
        assertEquals("When the deadline hasn't been reached tick() shouldn't write data", 1, transport.writes.size());

        pumpMockTransport(transport);

        // Receive Empty frame to satisfy local deadline
        processInput(transport,  ByteBuffer.wrap(new byte[] {0x00, 0x00, 0x00, 0x08, 0x02, 0x00, 0x00, 0x00}));

        deadline = transport.tick(expectedDeadline2);
        assertEquals("When the deadline has been reached expected a new local deadline to be returned", expectedDeadline3, deadline);
        assertEquals("When the deadline hasn't been reached tick() shouldn't write data", 1, transport.writes.size());

        pumpMockTransport(transport);

        assertEquals("Connection should be active", EndpointState.ACTIVE, connection.getLocalState());
        transport.tick(expectedDeadline3); // Wait for the deadline, but don't receive traffic, allow local timeout to expire
        assertEquals("Calling tick() after the deadline should result in the connection being closed", EndpointState.CLOSED, connection.getLocalState());
        assertEquals("tick() should have written data", 2, transport.writes.size());
        assertNotNull("should have written a non-empty frame", transport.writes.get(1));
        assertTrue("should have written a close frame", transport.writes.get(1) instanceof Close);
    }

    @Test
    public void testTickWithRemoteTimeout()
    {
        // all-positive
        doTickWithRemoteTimeoutTestImpl(4000, 10000, 14000, 18000, 22000);

        // all-negative
        doTickWithRemoteTimeoutTestImpl(2000, -100000, -98000, -96000, -94000);

        // negative to positive missing 0
        doTickWithRemoteTimeoutTestImpl(500, -950, -450, 50, 550);

        // negative to positive striking 0
        doTickWithRemoteTimeoutTestImpl(3000, -6000, -3000, 1, 3001);
    }

    private void doTickWithRemoteTimeoutTestImpl(int remoteTimeoutHalf, long tick1, long expectedDeadline1, long expectedDeadline2, long expectedDeadline3)
    {
        MockTransportImpl transport = new MockTransportImpl();
        Connection connection = Proton.connection();
        transport.bind(connection);

        connection.open();
        pumpMockTransport(transport);

        assertEquals("should have written data", 1, transport.writes.size());
        Object sentOpenFrame = transport.writes.get(0);
        assertNotNull("should have written a non-empty frame", sentOpenFrame);
        assertTrue("should have written an open frame", sentOpenFrame instanceof Open);
        assertNull("should not have had an idletimeout value", ((Open)sentOpenFrame).getIdleTimeOut());

        // Receive Protocol header
        processInput(transport, ByteBuffer.wrap(new byte[] {'A', 'M', 'Q', 'P', 0x00, 0x01, 0x00, 0x00}));

        // Handle the peer transmitting [half] their timeout. We half it on receipt to avoid spurious timeouts
        // if they not have transmitted half their actual timeout, as the AMQP spec only says they SHOULD do that.
        Open open = new Open();
        open.setIdleTimeOut(new UnsignedInteger(remoteTimeoutHalf * 2));
        TransportFrame openFrame = new TransportFrame(CHANNEL_ID, open, null);
        transport.handleFrame(openFrame);
        pumpMockTransport(transport);

        long deadline = transport.tick(tick1);
        assertEquals("Unexpected deadline returned", expectedDeadline1, deadline);

        // Wait for less time than the deadline with no data - get the same value
        long interimTick = tick1 + 10;
        assertTrue (interimTick < expectedDeadline1);
        assertEquals("When the deadline hasn't been reached tick() should return the previous deadline",  expectedDeadline1, transport.tick(interimTick));
        assertEquals("When the deadline hasn't been reached tick() shouldn't write data", 1, transport.writes.size());

        deadline = transport.tick(expectedDeadline1);
        assertEquals("When the deadline has been reached expected a new remote deadline to be returned", expectedDeadline2, deadline);
        assertEquals("tick() should have written data", 2, transport.writes.size());
        assertEquals("tick() should have written an empty frame", null, transport.writes.get(1));

        pumpMockTransport(transport);

        // Do some actual work, create real traffic, removing the need to send empty frame to satisfy idle-timeout
        connection.session().open();
        pumpMockTransport(transport);
        assertEquals("session open should have written data", 3, transport.writes.size());
        Object sentBeginFrame = transport.writes.get(2);
        assertNotNull("should have written a non-empty frame", sentBeginFrame);
        assertTrue("session open should have written a Begin frame", sentBeginFrame instanceof Begin);

        deadline = transport.tick(expectedDeadline2);
        assertEquals("When the deadline has been reached expected a new remote deadline to be returned", expectedDeadline3, deadline);
        assertEquals("tick() should not have written data as there was actual activity", 3, transport.writes.size());

        pumpMockTransport(transport);

        transport.tick(expectedDeadline3);
        assertEquals("tick() should have written data", 4, transport.writes.size());
        assertEquals("tick() should have written an empty frame", null, transport.writes.get(1));
    }

    @Test
    public void testTickWithBothTimeouts()
    {
        // all-positive
        doTickWithBothTimeoutsTestImpl(true, 5000, 2000, 10000, 12000, 14000, 15000);
        doTickWithBothTimeoutsTestImpl(false, 5000, 2000, 10000, 12000, 14000, 15000);

        // all-negative
        doTickWithBothTimeoutsTestImpl(true, 10000, 4000, -100000, -96000, -92000, -90000);
        doTickWithBothTimeoutsTestImpl(false, 10000, 4000, -100000, -96000, -92000, -90000);

        // negative to positive missing 0
        doTickWithBothTimeoutsTestImpl(true, 500, 200, -450, -250, -50, 50);
        doTickWithBothTimeoutsTestImpl(false, 500, 200, -450, -250, -50, 50);

        // negative to positive striking 0 with local deadline
        doTickWithBothTimeoutsTestImpl(true, 500, 200, -500, -300, -100, 1);
        doTickWithBothTimeoutsTestImpl(false, 500, 200, -500, -300, -100, 1);

        // negative to positive striking 0 with remote deadline
        doTickWithBothTimeoutsTestImpl(true, 500, 200, -200, 1, 201, 300);
        doTickWithBothTimeoutsTestImpl(false, 500, 200, -200, 1, 201, 300);
    }

    private void doTickWithBothTimeoutsTestImpl(boolean allowLocalTimeout, int localTimeout, int remoteTimeoutHalf, long tick1,
                                                long expectedDeadline1, long expectedDeadline2, long expectedDeadline3)
    {
        MockTransportImpl transport = new MockTransportImpl();
        Connection connection = Proton.connection();
        transport.bind(connection);

        // Set our local idleTimeout
        transport.setIdleTimeout(localTimeout);

        connection.open();
        pumpMockTransport(transport);

        assertEquals("should have written data", 1, transport.writes.size());
        assertNotNull("should have written a non-empty frame", transport.writes.get(0));
        assertTrue("should have written an open frame", transport.writes.get(0) instanceof Open);

        // Receive Protocol header
        processInput(transport, ByteBuffer.wrap(new byte[] {'A', 'M', 'Q', 'P', 0x00, 0x01, 0x00, 0x00}));

        // Handle the peer transmitting [half] their timeout. We half it on receipt to avoid spurious timeouts
        // if they not have transmitted half their actual timeout, as the AMQP spec only says they SHOULD do that.
        Open open = new Open();
        open.setIdleTimeOut(new UnsignedInteger(remoteTimeoutHalf * 2));
        TransportFrame openFrame = new TransportFrame(CHANNEL_ID, open, null);
        transport.handleFrame(openFrame);
        pumpMockTransport(transport);

        long deadline = transport.tick(tick1);
        assertEquals("Unexpected deadline returned", expectedDeadline1, deadline);

        // Wait for less time than the deadline with no data - get the same value
        long interimTick = tick1 + 10;
        assertTrue (interimTick < expectedDeadline1);
        assertEquals("When the deadline hasn't been reached tick() should return the previous deadline",  expectedDeadline1, transport.tick(interimTick));
        assertEquals("When the deadline hasn't been reached tick() shouldn't write data", 1, transport.writes.size());

        deadline = transport.tick(expectedDeadline1);
        assertEquals("When the deadline has been reached expected a new remote deadline to be returned", expectedDeadline2, deadline);
        assertEquals("tick() should have written data", 2, transport.writes.size());
        assertEquals("tick() should have written an empty frame", null, transport.writes.get(1));

        pumpMockTransport(transport);

        deadline = transport.tick(expectedDeadline2);
        assertEquals("When the deadline has been reached expected a new local deadline to be returned", expectedDeadline3, deadline);
        assertEquals("tick() should have written data", 3, transport.writes.size());
        assertEquals("tick() should have written an empty frame", null, transport.writes.get(2));

        pumpMockTransport(transport);

        if(allowLocalTimeout) {
            assertEquals("Connection should be active", EndpointState.ACTIVE, connection.getLocalState());
            transport.tick(expectedDeadline3); // Wait for the deadline, but don't receive traffic, allow local timeout to expire
            assertEquals("Calling tick() after the deadline should result in the connection being closed", EndpointState.CLOSED, connection.getLocalState());
            assertEquals("tick() should have written data", 4, transport.writes.size());
            assertNotNull("should have written a non-empty frame", transport.writes.get(3));
            assertTrue("should have written a close frame", transport.writes.get(3) instanceof Close);
        } else {
            // Receive Empty frame to satisfy local deadline
            processInput(transport,  ByteBuffer.wrap(new byte[] {0x00, 0x00, 0x00, 0x08, 0x02, 0x00, 0x00, 0x00}));

            deadline = transport.tick(expectedDeadline3);
            assertEquals("Receiving data should have reset the deadline (to the next remote one)",  expectedDeadline2 + (remoteTimeoutHalf), deadline);
            assertEquals("tick() shouldn't have written data", 3, transport.writes.size());
            assertEquals("Connection should be active", EndpointState.ACTIVE, connection.getLocalState());
        }
    }

    @Test
    public void testTickWithNanoTimeDerivedValueWhichWrapsLocalThenRemote()
    {
        doTickWithNanoTimeDerivedValueWhichWrapsLocalThenRemoteTestImpl(false);
    }

    @Test
    public void testTickWithNanoTimeDerivedValueWhichWrapsLocalThenRemoteWithLocalTimeout()
    {
        doTickWithNanoTimeDerivedValueWhichWrapsLocalThenRemoteTestImpl(true);
    }

    private void doTickWithNanoTimeDerivedValueWhichWrapsLocalThenRemoteTestImpl(boolean allowLocalTimeout) {
        int localTimeout = 5000;
        int remoteTimeoutHalf = 2000;
        assertTrue(remoteTimeoutHalf < localTimeout);

        long offset = 2500;
        assertTrue(offset < localTimeout);
        assertTrue(offset > remoteTimeoutHalf);

        MockTransportImpl transport = new MockTransportImpl();
        Connection connection = Proton.connection();
        transport.bind(connection);

        // Set our local idleTimeout
        transport.setIdleTimeout(localTimeout);

        connection.open();
        pumpMockTransport(transport);

        assertEquals("should have written data", 1, transport.writes.size());
        assertNotNull("should have written a non-empty frame", transport.writes.get(0));
        assertTrue("should have written an open frame", transport.writes.get(0) instanceof Open);

        // Receive Protocol header
        processInput(transport, ByteBuffer.wrap(new byte[] {'A', 'M', 'Q', 'P', 0x00, 0x01, 0x00, 0x00}));

        // Handle the peer transmitting [half] their timeout. We half it on receipt to avoid spurious timeouts
        // if they not have transmitted half their actual timeout, as the AMQP spec only says they SHOULD do that.
        Open open = new Open();
        open.setIdleTimeOut(new UnsignedInteger(remoteTimeoutHalf * 2));
        TransportFrame openFrame = new TransportFrame(CHANNEL_ID, open, null);
        transport.handleFrame(openFrame);
        pumpMockTransport(transport);

        long deadline = transport.tick(Long.MAX_VALUE - offset);
        assertEquals("Unexpected deadline returned", Long.MAX_VALUE - offset + remoteTimeoutHalf, deadline);

        deadline = transport.tick(Long.MAX_VALUE - (offset - 100));    // Wait for less time than the deadline with no data - get the same value
        assertEquals("When the deadline hasn't been reached tick() should return the previous deadline",  Long.MAX_VALUE -offset + remoteTimeoutHalf, deadline);
        assertEquals("When the deadline hasn't been reached tick() shouldn't write data", 1, transport.writes.size());

        deadline = transport.tick(Long.MAX_VALUE -offset + remoteTimeoutHalf); // Wait for the deadline - next deadline should be previous + remoteTimeoutHalf;
        assertEquals("When the deadline has been reached expected a new remote deadline to be returned", Long.MIN_VALUE + (2* remoteTimeoutHalf) - offset -1, deadline);
        assertEquals("tick() should have written data", 2, transport.writes.size());
        assertEquals("tick() should have written an empty frame", null, transport.writes.get(1));

        pumpMockTransport(transport);

        deadline = transport.tick(Long.MIN_VALUE + (2* remoteTimeoutHalf) - offset -1); // Wait for the deadline - next deadline should be orig + localTimeout;
        assertEquals("When the deadline has been reached expected a new local deadline to be returned", Long.MIN_VALUE + (localTimeout - offset) -1, deadline);
        assertEquals("tick() should have written data", 3, transport.writes.size());
        assertEquals("tick() should have written an empty frame", null, transport.writes.get(2));

        pumpMockTransport(transport);

        if(allowLocalTimeout) {
            assertEquals("Connection should be active", EndpointState.ACTIVE, connection.getLocalState());
            transport.tick(Long.MIN_VALUE + (localTimeout - offset) -1); // Wait for the deadline, but don't receive traffic, allow local timeout to expire
            assertEquals("Calling tick() after the deadline should result in the connection being closed", EndpointState.CLOSED, connection.getLocalState());
            assertEquals("tick() should have written data", 4, transport.writes.size());
            assertNotNull("should have written a non-empty frame", transport.writes.get(3));
            assertTrue("should have written a close frame", transport.writes.get(3) instanceof Close);
        } else {
            // Receive Empty frame to satisfy local deadline
            processInput(transport,  ByteBuffer.wrap(new byte[] {0x00, 0x00, 0x00, 0x08, 0x02, 0x00, 0x00, 0x00}));

            deadline = transport.tick(Long.MIN_VALUE + (localTimeout - offset) -1); // Wait for the deadline - next deadline should be orig + 3*remoteTimeoutHalf;
            assertEquals("Receiving data should have reset the deadline (to the remote one)",  Long.MIN_VALUE + (3* remoteTimeoutHalf) - offset -1, deadline);
            assertEquals("tick() shouldn't have written data", 3, transport.writes.size());
            assertEquals("Connection should be active", EndpointState.ACTIVE, connection.getLocalState());
        }
    }

    @Test
    public void testTickWithNanoTimeDerivedValueWhichWrapsRemoteThenLocal()
    {
        doTickWithNanoTimeDerivedValueWhichWrapsRemoteThenLocalTestImpl(false);
    }

    @Test
    public void testTickWithNanoTimeDerivedValueWhichWrapsRemoteThenLocalWithLocalTimeout()
    {
        doTickWithNanoTimeDerivedValueWhichWrapsRemoteThenLocalTestImpl(true);
    }

    private void doTickWithNanoTimeDerivedValueWhichWrapsRemoteThenLocalTestImpl(boolean allowLocalTimeout) {
        int localTimeout = 2000;
        int remoteTimeoutHalf = 5000;
        assertTrue(localTimeout < remoteTimeoutHalf);

        long offset = 2500;
        assertTrue(offset > localTimeout);
        assertTrue(offset < remoteTimeoutHalf);

        MockTransportImpl transport = new MockTransportImpl();
        Connection connection = Proton.connection();
        transport.bind(connection);

        // Set our local idleTimeout
        transport.setIdleTimeout(localTimeout);

        connection.open();
        pumpMockTransport(transport);

        assertEquals("should have written data", 1, transport.writes.size());
        assertNotNull("should have written a non-empty frame", transport.writes.get(0));
        assertTrue("should have written an open frame", transport.writes.get(0) instanceof Open);

        // Receive Protocol header
        processInput(transport, ByteBuffer.wrap(new byte[] {'A', 'M', 'Q', 'P', 0x00, 0x01, 0x00, 0x00}));

        // Handle the peer transmitting [half] their timeout. We half it on receipt to avoid spurious timeouts
        // if they not have transmitted half their actual timeout, as the AMQP spec only says they SHOULD do that.
        Open open = new Open();
        open.setIdleTimeOut(new UnsignedInteger(remoteTimeoutHalf * 2));
        TransportFrame openFrame = new TransportFrame(CHANNEL_ID, open, null);
        transport.handleFrame(openFrame);
        pumpMockTransport(transport);

        long deadline = transport.tick(Long.MAX_VALUE - offset);
        assertEquals("Unexpected deadline returned",  Long.MAX_VALUE - offset + localTimeout, deadline);

        deadline = transport.tick(Long.MAX_VALUE - (offset - 100));    // Wait for less time than the deadline with no data - get the same value
        assertEquals("When the deadline hasn't been reached tick() should return the previous deadline",  Long.MAX_VALUE - offset + localTimeout, deadline);
        assertEquals("tick() shouldn't have written data", 1, transport.writes.size());

        // Receive Empty frame to satisfy local deadline
        processInput(transport,  ByteBuffer.wrap(new byte[] {0x00, 0x00, 0x00, 0x08, 0x02, 0x00, 0x00, 0x00}));

        deadline = transport.tick(Long.MAX_VALUE - offset + localTimeout); // Wait for the deadline - next deadline should be orig + 2* localTimeout;
        assertEquals("When the deadline has been reached expected a new local deadline to be returned", Long.MIN_VALUE + (localTimeout - offset) -1 + localTimeout, deadline);
        assertEquals("tick() should not have written data", 1, transport.writes.size());

        pumpMockTransport(transport);

        if(allowLocalTimeout) {
            assertEquals("Connection should be active", EndpointState.ACTIVE, connection.getLocalState());
            transport.tick(Long.MIN_VALUE + (localTimeout - offset) -1 + localTimeout); // Wait for the deadline, but don't receive traffic, allow local timeout to expire
            assertEquals("Calling tick() after the deadline should result in the connection being closed", EndpointState.CLOSED, connection.getLocalState());
            assertEquals("tick() should have written data", 2, transport.writes.size());
            assertNotNull("should have written a non-empty frame", transport.writes.get(1));
            assertTrue("should have written a close frame", transport.writes.get(1) instanceof Close);
        } else {
            // Receive Empty frame to satisfy local deadline
            processInput(transport,  ByteBuffer.wrap(new byte[] {0x00, 0x00, 0x00, 0x08, 0x02, 0x00, 0x00, 0x00}));

            deadline = transport.tick(Long.MIN_VALUE + (localTimeout - offset) -1 + localTimeout); // Wait for the deadline - next deadline should be orig + remoteTimeoutHalf;
            assertEquals("Receiving data should have reset the deadline (to the remote one)",  Long.MIN_VALUE + remoteTimeoutHalf - offset -1, deadline);
            assertEquals("tick() shouldn't have written data", 1, transport.writes.size());

            deadline = transport.tick(Long.MIN_VALUE + remoteTimeoutHalf - offset -1); // Wait for the deadline - next deadline should be orig + 3* localTimeout;
            assertEquals("When the deadline has been reached expected a new local deadline to be returned", Long.MIN_VALUE + (3* localTimeout) - offset -1, deadline);
            assertEquals("tick() should have written data", 2, transport.writes.size());
            assertEquals("tick() should have written an empty frame", null, transport.writes.get(1));
            assertEquals("Connection should be active", EndpointState.ACTIVE, connection.getLocalState());
        }
    }

    @Test
    public void testTickWithNanoTimeDerivedValueWhichWrapsBothRemoteFirst()
    {
        doTickWithNanoTimeDerivedValueWhichWrapsBothRemoteFirstTestImpl(false);
    }

    @Test
    public void testTickWithNanoTimeDerivedValueWhichWrapsBothRemoteFirstWithLocalTimeout()
    {
        doTickWithNanoTimeDerivedValueWhichWrapsBothRemoteFirstTestImpl(true);
    }

    private void doTickWithNanoTimeDerivedValueWhichWrapsBothRemoteFirstTestImpl(boolean allowLocalTimeout) {
        int localTimeout = 2000;
        int remoteTimeoutHalf = 2500;
        assertTrue(localTimeout < remoteTimeoutHalf);

        long offset = 500;
        assertTrue(offset < localTimeout);

        MockTransportImpl transport = new MockTransportImpl();
        Connection connection = Proton.connection();
        transport.bind(connection);

        // Set our local idleTimeout
        transport.setIdleTimeout(localTimeout);

        connection.open();
        pumpMockTransport(transport);

        assertEquals("should have written data", 1, transport.writes.size());
        assertNotNull("should have written a non-empty frame", transport.writes.get(0));
        assertTrue("should have written an open frame", transport.writes.get(0) instanceof Open);

        // Receive Protocol header
        processInput(transport, ByteBuffer.wrap(new byte[] {'A', 'M', 'Q', 'P', 0x00, 0x01, 0x00, 0x00}));

        // Handle the peer transmitting [half] their timeout. We half it on receipt to avoid spurious timeouts
        // if they not have transmitted half their actual timeout, as the AMQP spec only says they SHOULD do that.
        Open open = new Open();
        open.setIdleTimeOut(new UnsignedInteger(remoteTimeoutHalf * 2));
        TransportFrame openFrame = new TransportFrame(CHANNEL_ID, open, null);
        transport.handleFrame(openFrame);
        pumpMockTransport(transport);

        long deadline = transport.tick(Long.MAX_VALUE - offset);
        assertEquals("Unexpected deadline returned",  Long.MIN_VALUE + (localTimeout - offset) -1, deadline);

        deadline = transport.tick(Long.MAX_VALUE - (offset - 100));    // Wait for less time than the deadline with no data - get the same value
        assertEquals("When the deadline hasn't been reached tick() should return the previous deadline",  Long.MIN_VALUE + (localTimeout - offset) -1, deadline);
        assertEquals("tick() shouldn't have written data", 1, transport.writes.size());

        // Receive Empty frame to satisfy local deadline
        processInput(transport,  ByteBuffer.wrap(new byte[] {0x00, 0x00, 0x00, 0x08, 0x02, 0x00, 0x00, 0x00}));

        deadline = transport.tick(Long.MIN_VALUE + (localTimeout - offset) -1); // Wait for the deadline - next deadline should be orig + remoteTimeoutHalf;
        assertEquals("When the deadline has been reached expected a new remote deadline to be returned", Long.MIN_VALUE + (remoteTimeoutHalf - offset) -1, deadline);
        assertEquals("When the deadline hasn't been reached tick() shouldn't write data", 1, transport.writes.size());

        deadline = transport.tick(Long.MIN_VALUE + (remoteTimeoutHalf - offset) -1); // Wait for the deadline - next deadline should be orig + 2* localTimeout;
        assertEquals("When the deadline has been reached expected a new local deadline to be returned", Long.MIN_VALUE + (localTimeout - offset) -1 + localTimeout, deadline);
        assertEquals("tick() should have written data", 2, transport.writes.size());
        assertEquals("tick() should have written an empty frame", null, transport.writes.get(1));

        pumpMockTransport(transport);

        if(allowLocalTimeout) {
            assertEquals("Connection should be active", EndpointState.ACTIVE, connection.getLocalState());
            transport.tick(Long.MIN_VALUE + (localTimeout - offset) -1 + localTimeout); // Wait for the deadline, but don't receive traffic, allow local timeout to expire
            assertEquals("Calling tick() after the deadline should result in the connection being closed", EndpointState.CLOSED, connection.getLocalState());
            assertEquals("tick() should have written data", 3, transport.writes.size());
            assertNotNull("should have written a non-empty frame", transport.writes.get(2));
            assertTrue("should have written a close frame", transport.writes.get(2) instanceof Close);
        } else {
            // Receive Empty frame to satisfy local deadline
            processInput(transport,  ByteBuffer.wrap(new byte[] {0x00, 0x00, 0x00, 0x08, 0x02, 0x00, 0x00, 0x00}));

            deadline = transport.tick(Long.MIN_VALUE + (localTimeout - offset) -1 + localTimeout); // Wait for the deadline - next deadline should be orig + 2*remoteTimeoutHalf;
            assertEquals("Receiving data should have reset the deadline (to the remote one)",  Long.MIN_VALUE + (2* remoteTimeoutHalf) - offset -1, deadline);
            assertEquals("tick() shouldn't have written data", 2, transport.writes.size());
            assertEquals("Connection should be active", EndpointState.ACTIVE, connection.getLocalState());
        }
    }

    @Test
    public void testTickWithNanoTimeDerivedValueWhichWrapsBothLocalFirst()
    {
        doTickWithNanoTimeDerivedValueWhichWrapsBothLocalFirstTestImpl(false);
    }

    @Test
    public void testTickWithNanoTimeDerivedValueWhichWrapsBothLocalFirstWithLocalTimeout()
    {
        doTickWithNanoTimeDerivedValueWhichWrapsBothLocalFirstTestImpl(true);
    }

    private void doTickWithNanoTimeDerivedValueWhichWrapsBothLocalFirstTestImpl(boolean allowLocalTimeout) {
        int localTimeout = 5000;
        int remoteTimeoutHalf = 2000;
        assertTrue(remoteTimeoutHalf < localTimeout);

        long offset = 500;
        assertTrue(offset < remoteTimeoutHalf);

        MockTransportImpl transport = new MockTransportImpl();
        Connection connection = Proton.connection();
        transport.bind(connection);

        // Set our local idleTimeout
        transport.setIdleTimeout(localTimeout);

        connection.open();
        while(transport.pending() > 0) {
            transport.pop(transport.head().remaining());
        }

        assertEquals("should have written data", 1, transport.writes.size());
        assertNotNull("should have written a non-empty frame", transport.writes.get(0));
        assertTrue("should have written an open frame", transport.writes.get(0) instanceof Open);

        // Handle the peer transmitting [half] their timeout. We half it on receipt to avoid spurious timeouts
        // if they not have transmitted half their actual timeout, as the AMQP spec only says they SHOULD do that.
        Open open = new Open();
        open.setIdleTimeOut(new UnsignedInteger(remoteTimeoutHalf * 2));
        TransportFrame openFrame = new TransportFrame(CHANNEL_ID, open, null);
        transport.handleFrame(openFrame);
        pumpMockTransport(transport);


        long deadline = transport.tick(Long.MAX_VALUE - offset);
        assertEquals("Unexpected deadline returned",  Long.MIN_VALUE + (remoteTimeoutHalf - offset) -1, deadline);

        deadline = transport.tick(Long.MAX_VALUE - (offset - 100));    // Wait for less time than the deadline with no data - get the same value
        assertEquals("When the deadline hasn't been reached tick() should return the previous deadline",  Long.MIN_VALUE + (remoteTimeoutHalf - offset) -1, deadline);
        assertEquals("When the deadline hasn't been reached tick() shouldn't write data", 1, transport.writes.size());

        deadline = transport.tick(Long.MIN_VALUE + (remoteTimeoutHalf - offset) -1); // Wait for the deadline - next deadline should be previous + remoteTimeoutHalf;
        assertEquals("When the deadline has been reached expected a new remote deadline to be returned", Long.MIN_VALUE + (remoteTimeoutHalf - offset) -1 + remoteTimeoutHalf, deadline);
        assertEquals("tick() should have written data", 2, transport.writes.size());
        assertEquals("tick() should have written an empty frame", null, transport.writes.get(1));

        pumpMockTransport(transport);

        deadline = transport.tick(Long.MIN_VALUE + (remoteTimeoutHalf - offset) -1 + remoteTimeoutHalf); // Wait for the deadline - next deadline should be orig + localTimeout;
        assertEquals("When the deadline has been reached expected a new local deadline to be returned", Long.MIN_VALUE + (localTimeout - offset) -1, deadline);
        assertEquals("tick() should have written data", 3, transport.writes.size());
        assertEquals("tick() should have written an empty frame", null, transport.writes.get(2));

        pumpMockTransport(transport);

        if(allowLocalTimeout) {
            assertEquals("Connection should be active", EndpointState.ACTIVE, connection.getLocalState());
            transport.tick(Long.MIN_VALUE + (localTimeout - offset) -1); // Wait for the deadline, but don't receive traffic, allow local timeout to expire
            assertEquals("Calling tick() after the deadline should result in the connection being closed", EndpointState.CLOSED, connection.getLocalState());
            assertEquals("tick() should have written data", 4, transport.writes.size());
            assertNotNull("should have written a non-empty frame", transport.writes.get(3));
            assertTrue("should have written a close frame", transport.writes.get(3) instanceof Close);
        } else {
            // Receive Empty frame to satisfy local deadline
            processInput(transport,  ByteBuffer.wrap(new byte[] {0x00, 0x00, 0x00, 0x08, 0x02, 0x00, 0x00, 0x00}));

            deadline = transport.tick(Long.MIN_VALUE + (localTimeout - offset) -1); // Wait for the deadline - next deadline should be orig + 3*remoteTimeoutHalf;
            assertEquals("Receiving data should have reset the deadline (to the remote one)",  Long.MIN_VALUE + (3* remoteTimeoutHalf) - offset -1, deadline);
            assertEquals("tick() shouldn't have written data", 3, transport.writes.size());
            assertEquals("Connection should be active", EndpointState.ACTIVE, connection.getLocalState());
        }
    }

    @Test
    public void testMaxFrameSizeOfPeerHasEffect()
    {
        doMaxFrameSizeTestImpl(0, 0, 5700, 1);
        doMaxFrameSizeTestImpl(1024, 0, 5700, 6);
    }

    @Test
    public void testMaxFrameSizeOutgoingFrameSizeLimitHasEffect()
    {
        doMaxFrameSizeTestImpl(0, 512, 5700, 12);
        doMaxFrameSizeTestImpl(1024, 512, 5700, 12);
        doMaxFrameSizeTestImpl(1024, 2048, 5700, 6);
    }

    void doMaxFrameSizeTestImpl(int remoteMaxFrameSize, int outboundFrameSizeLimit, int contentLength, int expectedNumFrames)
    {
        MockTransportImpl transport = new MockTransportImpl();
        transport.setEmitFlowEventOnSend(false);

        // If we have been given an outboundFrameSizeLimit, configure it
        if(outboundFrameSizeLimit != 0) {
            transport.setOutboundFrameSizeLimit(outboundFrameSizeLimit);
        }

        Connection connection = Proton.connection();
        transport.bind(connection);

        Session session = connection.session();
        session.open();

        String linkName = "mySender";
        Sender sender = session.sender(linkName);
        sender.open();

        String messageContent = createLargeContent(contentLength);
        sendMessage(sender, "tag1", messageContent);

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 0, transport.writes.size());

        // Now open the connection, expect the Open and Begin frames but
        // nothing else as we haven't opened the receiver itself yet.
        connection.open();

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 3, transport.writes.size());

        assertTrue("Unexpected frame type", transport.writes.get(0) instanceof Open);
        assertTrue("Unexpected frame type", transport.writes.get(1) instanceof Begin);
        assertTrue("Unexpected frame type", transport.writes.get(2) instanceof Attach);

        // Send the necessary responses to open/begin/attach then give sender credit
        Open open = new Open();
        if(remoteMaxFrameSize != 0) {
            open.setMaxFrameSize(UnsignedInteger.valueOf(remoteMaxFrameSize));
        }
        transport.handleFrame(new TransportFrame(0, open, null));

        Begin begin = new Begin();
        begin.setRemoteChannel(UnsignedShort.valueOf((short) 0));
        transport.handleFrame(new TransportFrame(0, begin, null));

        Attach attach = new Attach();
        attach.setHandle(UnsignedInteger.ZERO);
        attach.setRole(Role.RECEIVER);
        attach.setName(linkName);
        attach.setInitialDeliveryCount(UnsignedInteger.ZERO);
        transport.handleFrame(new TransportFrame(0, attach, null));

        Flow flow = new Flow();
        flow.setHandle(UnsignedInteger.ZERO);
        flow.setDeliveryCount(UnsignedInteger.ZERO);
        flow.setNextIncomingId(UnsignedInteger.ONE);
        flow.setNextOutgoingId(UnsignedInteger.ZERO);
        flow.setIncomingWindow(UnsignedInteger.valueOf(1024));
        flow.setOutgoingWindow(UnsignedInteger.valueOf(1024));
        flow.setLinkCredit(UnsignedInteger.valueOf(10));

        transport.handleFrame(new TransportFrame(0, flow, null));

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 3, transport.writes.size());

        // Now pump the transport again and expect transfers for the message
        pumpMockTransport(transport);

        // This calc isn't entirely precise, there is some added performative/frame overhead not
        // accounted for...but values are chosen to work, and verified here.
        final int frameCount;
        if(remoteMaxFrameSize == 0 && outboundFrameSizeLimit == 0) {
            frameCount = 1;
        } else if(remoteMaxFrameSize == 0 && outboundFrameSizeLimit != 0) {
            frameCount = (int) Math.ceil((double)contentLength / (double) outboundFrameSizeLimit);
        } else {
            int effectiveMaxFrameSize;
            if(outboundFrameSizeLimit != 0) {
                effectiveMaxFrameSize = Math.min(outboundFrameSizeLimit, remoteMaxFrameSize);
            } else {
                effectiveMaxFrameSize = remoteMaxFrameSize;
            }

            frameCount = (int) Math.ceil((double)contentLength / (double) effectiveMaxFrameSize);
        }

        assertEquals("Unexpected number of frames calculated", expectedNumFrames, frameCount);

        final int start = 3;
        final int totalExpected = start + frameCount;
        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), totalExpected, transport.writes.size());
        for(int i = start; i < totalExpected; i++) {
            assertTrue("Unexpected frame type", transport.writes.get(i) instanceof Transfer);
        }
    }

    private void processInput(MockTransportImpl transport, ByteBuffer data) {
        while (data.remaining() > 0)
        {
            int origLimit = data.limit();
            int amount = Math.min(transport.tail().remaining(), data.remaining());
            data.limit(data.position() + amount);
            transport.tail().put(data);
            data.limit(origLimit);
            transport.process();
        }
    }


    private static String createLargeContent(int length) {
        Random rand = new Random(System.currentTimeMillis());

        byte[] payload = new byte[length];
        for (int i = 0; i < length; i++) {
            payload[i] = (byte) (64 + 1 + rand.nextInt(9));
        }

        return new String(payload, StandardCharsets.UTF_8);
    }

    @Test
    public void testMultiplexMultiFrameDeliveryOnSingleSessionOutgoing() {
        doMultiplexMultiFrameDeliveryOnSingleSessionOutgoingTestImpl(false);
    }

    @Test
    public void testMultiplexMultiFrameDeliveriesOnSingleSessionOutgoing() {
        doMultiplexMultiFrameDeliveryOnSingleSessionOutgoingTestImpl(true);
    }

    private void doMultiplexMultiFrameDeliveryOnSingleSessionOutgoingTestImpl(boolean bothDeliveriesMultiFrame) {
        MockTransportImpl transport = new MockTransportImpl();
        transport.setEmitFlowEventOnSend(false);

        int contentLength1 = 6000;
        int frameSizeLimit = 4000;
        int contentLength2 = 2000;
        if(bothDeliveriesMultiFrame) {
            contentLength2 = 6000;
        }

        Connection connection = Proton.connection();
        transport.bind(connection);

        Session session = connection.session();
        session.open();

        String linkName = "mySender1";
        Sender sender = session.sender(linkName);
        sender.open();

        String linkName2 = "mySender2";
        Sender sender2 = session.sender(linkName2);
        sender2.open();

        String messageContent1 = createLargeContent(contentLength1);
        sendMessage(sender, "tag1", messageContent1);

        String messageContent2 = createLargeContent(contentLength2);
        sendMessage(sender2, "tag2", messageContent2);

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 0, transport.writes.size());

        // Now open the connection, expect the Open Begin, and Attach frames but
        // nothing else as we haven't remotely opened the receiver or given credit yet.
        connection.open();

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 4, transport.writes.size());

        assertTrue("Unexpected frame type", transport.writes.get(0) instanceof Open);
        assertTrue("Unexpected frame type", transport.writes.get(1) instanceof Begin);
        assertTrue("Unexpected frame type", transport.writes.get(2) instanceof Attach);
        assertTrue("Unexpected frame type", transport.writes.get(3) instanceof Attach);

        // Send the necessary responses to open/begin/attach then give senders credit
        Open open = new Open();
        open.setMaxFrameSize(UnsignedInteger.valueOf(frameSizeLimit));

        transport.handleFrame(new TransportFrame(0, open, null));

        Begin begin = new Begin();
        begin.setRemoteChannel(UnsignedShort.valueOf((short) 0));
        transport.handleFrame(new TransportFrame(0, begin, null));

        Attach attach1 = new Attach();
        attach1.setHandle(UnsignedInteger.ZERO);
        attach1.setRole(Role.RECEIVER);
        attach1.setName(linkName);
        attach1.setInitialDeliveryCount(UnsignedInteger.ZERO);

        transport.handleFrame(new TransportFrame(0, attach1, null));

        Attach attach2 = new Attach();
        attach2.setHandle(UnsignedInteger.ONE);
        attach2.setRole(Role.RECEIVER);
        attach2.setName(linkName2);
        attach2.setInitialDeliveryCount(UnsignedInteger.ZERO);

        transport.handleFrame(new TransportFrame(0, attach2, null));

        Flow flow1 = new Flow();
        flow1.setHandle(UnsignedInteger.ZERO);
        flow1.setDeliveryCount(UnsignedInteger.ZERO);
        flow1.setNextIncomingId(UnsignedInteger.ONE);
        flow1.setNextOutgoingId(UnsignedInteger.ZERO);
        flow1.setIncomingWindow(UnsignedInteger.valueOf(1024));
        flow1.setOutgoingWindow(UnsignedInteger.valueOf(1024));
        flow1.setLinkCredit(UnsignedInteger.valueOf(10));

        transport.handleFrame(new TransportFrame(0, flow1, null));

        Flow flow2 = new Flow();
        flow2.setHandle(UnsignedInteger.ONE);
        flow2.setDeliveryCount(UnsignedInteger.ZERO);
        flow2.setNextIncomingId(UnsignedInteger.ONE);
        flow2.setNextOutgoingId(UnsignedInteger.ZERO);
        flow2.setIncomingWindow(UnsignedInteger.valueOf(1024));
        flow2.setOutgoingWindow(UnsignedInteger.valueOf(1024));
        flow2.setLinkCredit(UnsignedInteger.valueOf(10));

        transport.handleFrame(new TransportFrame(0, flow2, null));

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 4, transport.writes.size());

        // Now pump the transport again and expect transfers for the messages
        pumpMockTransport(transport);

        int expectedFrames = bothDeliveriesMultiFrame ? 8 : 7;
        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), expectedFrames, transport.writes.size());

        FrameBody frameBody = transport.writes.get(4);
        assertTrue("Unexpected frame type", frameBody instanceof Transfer);
        Transfer transfer = (Transfer) frameBody;
        assertEquals("Unexpected delivery tag", new Binary("tag1".getBytes(StandardCharsets.UTF_8)), transfer.getDeliveryTag());
        assertEquals("Unexpected deliveryId", UnsignedInteger.ZERO, transfer.getDeliveryId());
        assertEquals("Unexpected more flag", true, transfer.getMore());

        frameBody = transport.writes.get(5);
        assertTrue("Unexpected frame type", frameBody instanceof Transfer);
        transfer = (Transfer) frameBody;
        assertEquals("Unexpected delivery tag", new Binary("tag2".getBytes(StandardCharsets.UTF_8)), transfer.getDeliveryTag());
        assertEquals("Unexpected deliveryId", UnsignedInteger.ONE, transfer.getDeliveryId());
        assertEquals("Unexpected more flag", bothDeliveriesMultiFrame, transfer.getMore());

        frameBody = transport.writes.get(6);
        assertTrue("Unexpected frame type", frameBody instanceof Transfer);
        transfer = (Transfer) frameBody;
        assertEquals("Unexpected delivery tag", new Binary("tag1".getBytes(StandardCharsets.UTF_8)), transfer.getDeliveryTag());
        assertEquals("Unexpected deliveryId", UnsignedInteger.ZERO, transfer.getDeliveryId());
        assertEquals("Unexpected more flag", false, transfer.getMore());

        if(bothDeliveriesMultiFrame) {
            frameBody = transport.writes.get(7);
            assertTrue("Unexpected frame type", frameBody instanceof Transfer);
            transfer = (Transfer) frameBody;
            assertEquals("Unexpected delivery tag", new Binary("tag2".getBytes(StandardCharsets.UTF_8)), transfer.getDeliveryTag());
            assertEquals("Unexpected deliveryId", UnsignedInteger.ONE, transfer.getDeliveryId());
            assertEquals("Unexpected more flag", false, transfer.getMore());
        }
    }

    @Test
    public void testMultiplexMultiFrameDeliveriesOnSingleSessionIncoming() {
        doMultiplexMultiFrameDeliveryOnSingleSessionIncomingTestImpl(true);
    }

    @Test
    public void testMultiplexMultiFrameDeliveryOnSingleSessionIncoming() {
        doMultiplexMultiFrameDeliveryOnSingleSessionIncomingTestImpl(false);
    }

    private void doMultiplexMultiFrameDeliveryOnSingleSessionIncomingTestImpl(boolean bothDeliveriesMultiFrame) {
        int contentLength1 = 7000;
        int maxPayloadChunkSize = 2000;
        int contentLength2 = 1000;
        if(bothDeliveriesMultiFrame) {
            contentLength2 = 4100;
        }

        MockTransportImpl transport = new MockTransportImpl();
        transport.setEmitFlowEventOnSend(false);
        Connection connection = Proton.connection();
        transport.bind(connection);

        connection.open();

        Session session = connection.session();
        session.open();

        String linkName1 = "myReceiver1";
        Receiver receiver1 = session.receiver(linkName1);
        receiver1.flow(5);
        receiver1.open();

        String linkName2 = "myReceiver2";
        Receiver receiver2 = session.receiver(linkName2);
        receiver2.flow(5);
        receiver2.open();

        pumpMockTransport(transport);

        final UnsignedInteger r1handle = UnsignedInteger.ZERO;
        final UnsignedInteger r2handle = UnsignedInteger.ONE;

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 6, transport.writes.size());

        assertTrue("Unexpected frame type", transport.writes.get(0) instanceof Open);
        assertTrue("Unexpected frame type", transport.writes.get(1) instanceof Begin);
        FrameBody frame = transport.writes.get(2);
        assertTrue("Unexpected frame type", frame instanceof Attach);
        assertEquals("Unexpected handle", ((Attach) frame).getHandle(), r1handle);
        frame = transport.writes.get(3);
        assertTrue("Unexpected frame type", frame instanceof Attach);
        assertEquals("Unexpected handle", ((Attach) frame).getHandle(), r2handle);
        frame = transport.writes.get(4);
        assertTrue("Unexpected frame type", frame instanceof Flow);
        assertEquals("Unexpected handle", ((Flow) frame).getHandle(), r1handle);
        frame = transport.writes.get(5);
        assertTrue("Unexpected frame type", frame instanceof Flow);
        assertEquals("Unexpected handle", ((Flow) frame).getHandle(), r2handle);

        assertNull("Should not yet have a delivery", receiver1.current());
        assertNull("Should not yet have a delivery", receiver2.current());

        // Send the necessary responses to open/begin/attach
        transport.handleFrame(new TransportFrame(0, new Open(), null));

        Begin begin = new Begin();
        begin.setRemoteChannel(UnsignedShort.valueOf((short) 0));
        begin.setNextOutgoingId(UnsignedInteger.ONE);
        begin.setIncomingWindow(UnsignedInteger.valueOf(1024));
        begin.setOutgoingWindow(UnsignedInteger.valueOf(1024));
        transport.handleFrame(new TransportFrame(0, begin, null));

        Attach attach1 = new Attach();
        attach1.setHandle(r1handle);
        attach1.setRole(Role.SENDER);
        attach1.setName(linkName1);
        attach1.setInitialDeliveryCount(UnsignedInteger.ZERO);
        transport.handleFrame(new TransportFrame(0, attach1, null));

        Attach attach2 = new Attach();
        attach2.setHandle(r2handle);
        attach2.setRole(Role.SENDER);
        attach2.setName(linkName2);
        attach2.setInitialDeliveryCount(UnsignedInteger.ZERO);
        transport.handleFrame(new TransportFrame(0, attach2, null));

        String deliveryTag1 = "tag1";
        String messageContent1 = createLargeContent(contentLength1);
        String deliveryTag2 = "tag2";
        String messageContent2 = createLargeContent(contentLength2);

        ArrayList<byte[]> message1chunks = createTransferPayloads(messageContent1, maxPayloadChunkSize);
        assertEquals("unexpected number of payload chunks", 4, message1chunks.size());
        ArrayList<byte[]> message2chunks = createTransferPayloads(messageContent2, maxPayloadChunkSize);
        if(bothDeliveriesMultiFrame) {
            assertEquals("unexpected number of payload chunks", 3, message2chunks.size());
        } else {
            assertEquals("unexpected number of payload chunks", 1, message2chunks.size());
        }

        while (true) {
           if (!message1chunks.isEmpty()) {
              byte[] chunk = message1chunks.remove(0);
              handlePartialTransfer(transport, r1handle, 1, deliveryTag1, chunk, !message1chunks.isEmpty());
           }

           if (!message2chunks.isEmpty()) {
               byte[] chunk = message2chunks.remove(0);
               handlePartialTransfer(transport, r2handle, 2, deliveryTag2, chunk, !message2chunks.isEmpty());
            }

           if (message1chunks.isEmpty() && message2chunks.isEmpty()) {
              break;
           }
        }

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 6, transport.writes.size());

        assertEquals("Unexpected queued count", 1, receiver1.getQueued());
        Delivery delivery1 = verifyDelivery(receiver1, deliveryTag1, messageContent1);
        assertNotNull("Should now have a delivery", delivery1);
        assertEquals("Unexpected queued count", 0, receiver1.getQueued());

        assertEquals("Unexpected queued count", 1, receiver2.getQueued());
        Delivery delivery2 = verifyDelivery(receiver2, deliveryTag2, messageContent2);
        assertNotNull("Should now have a delivery", delivery2);
        assertEquals("Unexpected queued count", 0, receiver2.getQueued());

        delivery1.disposition(Accepted.getInstance());
        delivery1.settle();
        pumpMockTransport(transport);
        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 7, transport.writes.size());

        frame = transport.writes.get(6);
        assertTrue("Unexpected frame type", frame instanceof Disposition);
        assertEquals("Unexpected delivery id", ((Disposition) frame).getFirst(), UnsignedInteger.ONE);
        assertEquals("Unexpected delivery id", ((Disposition) frame).getLast(), UnsignedInteger.ONE);

        delivery2.disposition(Accepted.getInstance());
        delivery2.settle();
        pumpMockTransport(transport);
        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 8, transport.writes.size());

        frame = transport.writes.get(7);
        assertTrue("Unexpected frame type", frame instanceof Disposition);
        assertEquals("Unexpected delivery id", ((Disposition) frame).getFirst(), UnsignedInteger.valueOf(2));
        assertEquals("Unexpected delivery id", ((Disposition) frame).getLast(), UnsignedInteger.valueOf(2));
    }

    private void handlePartialTransfer(TransportImpl transport, UnsignedInteger handle, int deliveryId, String deliveryTag, byte[] partialPayload, boolean more)
    {
        handlePartialTransfer(transport, handle, UnsignedInteger.valueOf(deliveryId), deliveryTag, partialPayload, more);
    }

    private void handlePartialTransfer(TransportImpl transport, UnsignedInteger handle, UnsignedInteger deliveryId, String deliveryTag, byte[] partialPayload, boolean more)
    {
        handlePartialTransfer(transport, handle, deliveryId, deliveryTag, partialPayload, more, false);
    }

    private void handlePartialTransfer(TransportImpl transport, UnsignedInteger handle, UnsignedInteger deliveryId, String deliveryTag, byte[] partialPayload, boolean more, boolean aborted)
    {
        handlePartialTransfer(transport, handle, deliveryId, deliveryTag, partialPayload, more, aborted, null);
    }

    private void handlePartialTransfer(TransportImpl transport, UnsignedInteger handle, UnsignedInteger deliveryId, String deliveryTag, byte[] partialPayload, boolean more, boolean aborted, Boolean settled)
    {
        byte[] tag = deliveryTag.getBytes(StandardCharsets.UTF_8);

        Transfer transfer = new Transfer();
        transfer.setHandle(handle);
        transfer.setDeliveryTag(new Binary(tag));
        transfer.setMessageFormat(UnsignedInteger.valueOf(DeliveryImpl.DEFAULT_MESSAGE_FORMAT));
        transfer.setMore(more);
        transfer.setAborted(aborted);
        if(deliveryId != null) {
            // Can be omitted in continuation frames for a given delivery.
            transfer.setDeliveryId(deliveryId);
        }
        if(settled != null) {
            transfer.setSettled(settled);
        }

        transport.handleFrame(new TransportFrame(0, transfer, new Binary(partialPayload, 0, partialPayload.length)));
    }

    private ArrayList<byte[]> createTransferPayloads(String content, int payloadChunkSize)
    {
        ArrayList<byte[]> payloadChunks = new ArrayList<>();

        Message m = Message.Factory.create();
        m.setBody(new AmqpValue(content));

        byte[] encoded = new byte[BUFFER_SIZE];
        int len = m.encode(encoded, 0, BUFFER_SIZE);
        assertTrue("given array was too small", len < BUFFER_SIZE);

        int copied = 0;
        while(copied < len) {
            int chunkSize = Math.min(len - copied, payloadChunkSize);
            byte[] chunk = new byte[chunkSize];

            System.arraycopy(encoded, copied, chunk, 0, chunkSize);

            payloadChunks.add(chunk);
            copied += chunkSize;
        }

        assertFalse("no payload chunks to return", payloadChunks.isEmpty());

        return payloadChunks;
    }

    @Test
    public void testDeliveryIdOutOfSequenceCausesISE() {
        MockTransportImpl transport = new MockTransportImpl();
        transport.setEmitFlowEventOnSend(false);
        Connection connection = Proton.connection();
        transport.bind(connection);

        connection.open();

        Session session = connection.session();
        session.open();

        String linkName1 = "myReceiver1";
        Receiver receiver1 = session.receiver(linkName1);
        receiver1.flow(5);
        receiver1.open();

        String linkName2 = "myReceiver2";
        Receiver receiver2 = session.receiver(linkName2);
        receiver2.flow(5);
        receiver2.open();

        pumpMockTransport(transport);

        final UnsignedInteger r1handle = UnsignedInteger.ZERO;
        final UnsignedInteger r2handle = UnsignedInteger.ONE;

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 6, transport.writes.size());

        // Give the necessary responses to open/begin/attach
        transport.handleFrame(new TransportFrame(0, new Open(), null));

        Begin begin = new Begin();
        begin.setRemoteChannel(UnsignedShort.valueOf((short) 0));
        begin.setNextOutgoingId(UnsignedInteger.ONE);
        begin.setIncomingWindow(UnsignedInteger.valueOf(1024));
        begin.setOutgoingWindow(UnsignedInteger.valueOf(1024));
        transport.handleFrame(new TransportFrame(0, begin, null));

        Attach attach1 = new Attach();
        attach1.setHandle(r1handle);
        attach1.setRole(Role.SENDER);
        attach1.setName(linkName1);
        attach1.setInitialDeliveryCount(UnsignedInteger.ZERO);
        transport.handleFrame(new TransportFrame(0, attach1, null));

        Attach attach2 = new Attach();
        attach2.setHandle(r2handle);
        attach2.setRole(Role.SENDER);
        attach2.setName(linkName2);
        attach2.setInitialDeliveryCount(UnsignedInteger.ZERO);
        transport.handleFrame(new TransportFrame(0, attach2, null));

        String deliveryTag1 = "tag1";
        String deliveryTag2 = "tag2";

        handlePartialTransfer(transport, r2handle, 2, deliveryTag2, new byte[]{ 2 }, false);
        try {
            handlePartialTransfer(transport, r1handle, 1, deliveryTag1, new byte[]{ 1 }, false);
            fail("Expected an ISE");
        } catch(IllegalStateException ise) {
            // Expected
            assertTrue("Unexpected exception:" + ise, ise.getMessage().contains("Expected delivery-id 3, got 1"));
        }
    }

    @Test
    public void testDeliveryIdMissingOnInitialTransferCausesISE() {
        MockTransportImpl transport = new MockTransportImpl();
        transport.setEmitFlowEventOnSend(false);
        Connection connection = Proton.connection();
        transport.bind(connection);

        connection.open();

        Session session = connection.session();
        session.open();

        String linkName1 = "myReceiver1";
        Receiver receiver1 = session.receiver(linkName1);
        receiver1.flow(5);
        receiver1.open();

        pumpMockTransport(transport);

        final UnsignedInteger r1handle = UnsignedInteger.ZERO;

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 4, transport.writes.size());

        // Give the necessary responses to open/begin/attach
        transport.handleFrame(new TransportFrame(0, new Open(), null));

        Begin begin = new Begin();
        begin.setRemoteChannel(UnsignedShort.valueOf((short) 0));
        begin.setNextOutgoingId(UnsignedInteger.ONE);
        begin.setIncomingWindow(UnsignedInteger.valueOf(1024));
        begin.setOutgoingWindow(UnsignedInteger.valueOf(1024));
        transport.handleFrame(new TransportFrame(0, begin, null));

        Attach attach1 = new Attach();
        attach1.setHandle(r1handle);
        attach1.setRole(Role.SENDER);
        attach1.setName(linkName1);
        attach1.setInitialDeliveryCount(UnsignedInteger.ZERO);
        transport.handleFrame(new TransportFrame(0, attach1, null));

        // Receive a delivery without any delivery-id on the [first] transfer frame, expect it to fail.
        try {
            handlePartialTransfer(transport, r1handle, null, "tag1", new byte[]{ 1 }, false);
            fail("Expected an ISE");
        } catch(IllegalStateException ise) {
            // Expected
            assertEquals("Unexpected message", "No delivery-id specified on first Transfer of new delivery", ise.getMessage());
        }
    }

    @Test
    public void testMultiplexDeliveriesOnSameReceiverLinkCausesISE() {
        MockTransportImpl transport = new MockTransportImpl();
        transport.setEmitFlowEventOnSend(false);
        Connection connection = Proton.connection();
        transport.bind(connection);

        connection.open();

        Session session = connection.session();
        session.open();

        String linkName1 = "myReceiver1";
        Receiver receiver1 = session.receiver(linkName1);
        receiver1.flow(5);
        receiver1.open();

        pumpMockTransport(transport);

        final UnsignedInteger r1handle = UnsignedInteger.ZERO;

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 4, transport.writes.size());

        // Give the necessary responses to open/begin/attach
        transport.handleFrame(new TransportFrame(0, new Open(), null));

        Begin begin = new Begin();
        begin.setRemoteChannel(UnsignedShort.valueOf((short) 0));
        begin.setNextOutgoingId(UnsignedInteger.ONE);
        begin.setIncomingWindow(UnsignedInteger.valueOf(1024));
        begin.setOutgoingWindow(UnsignedInteger.valueOf(1024));
        transport.handleFrame(new TransportFrame(0, begin, null));

        Attach attach1 = new Attach();
        attach1.setHandle(r1handle);
        attach1.setRole(Role.SENDER);
        attach1.setName(linkName1);
        attach1.setInitialDeliveryCount(UnsignedInteger.ZERO);
        transport.handleFrame(new TransportFrame(0, attach1, null));

        // Receive first transfer for a multi-frame delivery
        handlePartialTransfer(transport, r1handle, 1, "tag1", new byte[]{ 1 }, true);

        // Receive first transfer for ANOTHER multi-frame delivery, expect it to fail
        // as multiplexing deliveries on a single link is forbidden by the spec.
        try {
            handlePartialTransfer(transport, r1handle, 2, "tag2", new byte[]{ 2 }, true);
            fail("Expected an ISE");
        } catch(IllegalStateException ise) {
            // Expected
            assertEquals("Unexpected message", "Illegal multiplex of deliveries on same link with delivery-id 1 and 2", ise.getMessage());
        }
    }

    @Test
    public void testDeliveryIdTrackingHandlesAbortedDelivery() {
        MockTransportImpl transport = new MockTransportImpl();
        transport.setEmitFlowEventOnSend(false);
        Connection connection = Proton.connection();
        transport.bind(connection);

        connection.open();

        Session session = connection.session();
        session.open();

        String linkName1 = "myReceiver1";
        Receiver receiver1 = session.receiver(linkName1);
        receiver1.flow(5);
        receiver1.open();

        pumpMockTransport(transport);

        final UnsignedInteger r1handle = UnsignedInteger.ZERO;

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 4, transport.writes.size());

        // Give the necessary responses to open/begin/attach
        transport.handleFrame(new TransportFrame(0, new Open(), null));

        Begin begin = new Begin();
        begin.setRemoteChannel(UnsignedShort.valueOf((short) 0));
        begin.setNextOutgoingId(UnsignedInteger.ONE);
        begin.setIncomingWindow(UnsignedInteger.valueOf(1024));
        begin.setOutgoingWindow(UnsignedInteger.valueOf(1024));
        transport.handleFrame(new TransportFrame(0, begin, null));

        Attach attach1 = new Attach();
        attach1.setHandle(r1handle);
        attach1.setRole(Role.SENDER);
        attach1.setName(linkName1);
        attach1.setInitialDeliveryCount(UnsignedInteger.ZERO);
        transport.handleFrame(new TransportFrame(0, attach1, null));

        // Receive first transfer for a multi-frame delivery
        assertEquals("Unexpected queued count", 0, receiver1.getQueued());
        handlePartialTransfer(transport, r1handle, UnsignedInteger.ZERO, "tag1", new byte[]{ 1 }, true);
        assertEquals("Unexpected queued count", 1, receiver1.getQueued());
        // Receive second transfer for a multi-frame delivery, indicating it is aborted
        handlePartialTransfer(transport, r1handle, UnsignedInteger.ZERO, "tag1", new byte[]{ 2 }, true, true);
        assertEquals("Unexpected queued count", 1, receiver1.getQueued());

        // Receive first transfer for ANOTHER delivery, expect it not to fail, since the earlier delivery aborted
        handlePartialTransfer(transport, r1handle, UnsignedInteger.ONE, "tag2", new byte[]{ 3 }, false);
        assertEquals("Unexpected queued count", 2, receiver1.getQueued());

        receiver1.advance();
        verifyDeliveryRawPayload(receiver1, "tag2", new byte[] { 3 });
    }

    @Test
    public void testDeliveryWithIdOmittedOnContinuationTransfers() {
        MockTransportImpl transport = new MockTransportImpl();
        transport.setEmitFlowEventOnSend(false);
        Connection connection = Proton.connection();
        transport.bind(connection);

        connection.open();

        Session session = connection.session();
        session.open();

        String linkName1 = "myReceiver1";
        Receiver receiver1 = session.receiver(linkName1);
        receiver1.flow(5);
        receiver1.open();

        String linkName2 = "myReceiver2";
        Receiver receiver2 = session.receiver(linkName2);
        receiver2.flow(5);
        receiver2.open();

        pumpMockTransport(transport);

        final UnsignedInteger r1handle = UnsignedInteger.ZERO;
        final UnsignedInteger r2handle = UnsignedInteger.ONE;

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 6, transport.writes.size());

        // Give the necessary responses to open/begin/attach
        transport.handleFrame(new TransportFrame(0, new Open(), null));

        Begin begin = new Begin();
        begin.setRemoteChannel(UnsignedShort.valueOf((short) 0));
        begin.setNextOutgoingId(UnsignedInteger.ONE);
        begin.setIncomingWindow(UnsignedInteger.valueOf(1024));
        begin.setOutgoingWindow(UnsignedInteger.valueOf(1024));
        transport.handleFrame(new TransportFrame(0, begin, null));

        Attach attach1 = new Attach();
        attach1.setHandle(r1handle);
        attach1.setRole(Role.SENDER);
        attach1.setName(linkName1);
        attach1.setInitialDeliveryCount(UnsignedInteger.ZERO);
        transport.handleFrame(new TransportFrame(0, attach1, null));

        Attach attach2 = new Attach();
        attach2.setHandle(r2handle);
        attach2.setRole(Role.SENDER);
        attach2.setName(linkName2);
        attach2.setInitialDeliveryCount(UnsignedInteger.ZERO);
        transport.handleFrame(new TransportFrame(0, attach2, null));

        String deliveryTag1 = "tag1";
        String deliveryTag2 = "tag2";

        // Send multi-frame deliveries for each link, multiplexed together, and omit
        // the delivery-id on the continuation frames as allowed for by the spec.
        handlePartialTransfer(transport, r1handle, 1, deliveryTag1, new byte[]{ 1 }, true);
        handlePartialTransfer(transport, r2handle, 2, deliveryTag2, new byte[]{ 101 }, true);
        handlePartialTransfer(transport, r2handle, null, deliveryTag2, new byte[]{ 102 }, true);
        handlePartialTransfer(transport, r1handle, null, deliveryTag1, new byte[]{ 2 }, true);
        handlePartialTransfer(transport, r1handle, null, deliveryTag1, new byte[]{ 3 }, false);
        handlePartialTransfer(transport, r2handle, null, deliveryTag2, new byte[]{ 103 }, true);
        handlePartialTransfer(transport, r2handle, null, deliveryTag2, new byte[]{ 104 }, false);

        // Verify the transfer frames were all matched to compose the expected delivery payload.
        verifyDeliveryRawPayload(receiver1, deliveryTag1, new byte[] { 1, 2, 3 });
        verifyDeliveryRawPayload(receiver2, deliveryTag2, new byte[] { 101, 102, 103, 104 });
    }

    @Test
    public void testDeliveryIdThresholdsAndWraps() {
        // Check start from 0
        doDeliveryIdThresholdsWrapsTestImpl(UnsignedInteger.ZERO, UnsignedInteger.ONE, UnsignedInteger.valueOf(2));
        // Check run up to max-int (interesting boundary for underlying impl)
        doDeliveryIdThresholdsWrapsTestImpl(UnsignedInteger.valueOf(Integer.MAX_VALUE - 2), UnsignedInteger.valueOf(Integer.MAX_VALUE -1), UnsignedInteger.valueOf(Integer.MAX_VALUE));
        // Check crossing from signed range value into unsigned range value (interesting boundary for underlying impl)
        long maxIntAsLong = Integer.MAX_VALUE;
        doDeliveryIdThresholdsWrapsTestImpl(UnsignedInteger.valueOf(maxIntAsLong), UnsignedInteger.valueOf(maxIntAsLong + 1L), UnsignedInteger.valueOf(maxIntAsLong + 2L));
        // Check run up to max-uint
        doDeliveryIdThresholdsWrapsTestImpl(UnsignedInteger.valueOf(0xFFFFFFFFL - 2), UnsignedInteger.valueOf(0xFFFFFFFFL - 1), UnsignedInteger.MAX_VALUE);
        // Check wrapping from max unsigned value back to min(/0).
        doDeliveryIdThresholdsWrapsTestImpl(UnsignedInteger.MAX_VALUE, UnsignedInteger.ZERO, UnsignedInteger.ONE);
    }

    private void doDeliveryIdThresholdsWrapsTestImpl(UnsignedInteger deliveryId1, UnsignedInteger deliveryId2, UnsignedInteger deliveryId3) {
        MockTransportImpl transport = new MockTransportImpl();
        transport.setEmitFlowEventOnSend(false);
        Connection connection = Proton.connection();
        transport.bind(connection);

        connection.open();

        Session session = connection.session();
        session.open();

        String linkName1 = "myReceiver1";
        Receiver receiver1 = session.receiver(linkName1);
        receiver1.flow(5);
        receiver1.open();

        pumpMockTransport(transport);

        final UnsignedInteger r1handle = UnsignedInteger.ZERO;

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 4, transport.writes.size());

        // Give the necessary responses to open/begin/attach
        transport.handleFrame(new TransportFrame(0, new Open(), null));

        Begin begin = new Begin();
        begin.setRemoteChannel(UnsignedShort.valueOf((short) 0));
        begin.setNextOutgoingId(UnsignedInteger.ONE);
        begin.setIncomingWindow(UnsignedInteger.valueOf(1024));
        begin.setOutgoingWindow(UnsignedInteger.valueOf(1024));
        transport.handleFrame(new TransportFrame(0, begin, null));

        Attach attach1 = new Attach();
        attach1.setHandle(r1handle);
        attach1.setRole(Role.SENDER);
        attach1.setName(linkName1);
        attach1.setInitialDeliveryCount(UnsignedInteger.ZERO);
        transport.handleFrame(new TransportFrame(0, attach1, null));

        String deliveryTag1 = "tag1";
        String deliveryTag2 = "tag2";
        String deliveryTag3 = "tag3";

        // Send deliveries with the given delivery-id
        handlePartialTransfer(transport, r1handle, deliveryId1, deliveryTag1, new byte[]{ 1 }, false);
        handlePartialTransfer(transport, r1handle, deliveryId2, deliveryTag2, new byte[]{ 2 }, false);
        handlePartialTransfer(transport, r1handle, deliveryId3, deliveryTag3, new byte[]{ 3 }, false);

        // Verify deliveries arrived with expected payload
        verifyDeliveryRawPayload(receiver1, deliveryTag1, new byte[] { 1 });
        verifyDeliveryRawPayload(receiver1, deliveryTag2, new byte[] { 2 });
        verifyDeliveryRawPayload(receiver1, deliveryTag3, new byte[] { 3 });
    }

    @Test
    public void testAbortedDelivery() {
        // Check aborted=true, more=false, settled=true.
        doAbortedDeliveryTestImpl(false, true);
        // Check aborted=true, more=false, settled=unset(false)
        // Aborted overrides settled not being set.
        doAbortedDeliveryTestImpl(false, null);
        // Check aborted=true, more=false, settled=false
        // Aborted overrides settled being explicitly false.
        doAbortedDeliveryTestImpl(false, false);

        // Check aborted=true, more=true, settled=true
        // Aborted overrides the more=true.
        doAbortedDeliveryTestImpl(true, true);
        // Check aborted=true, more=true, settled=unset(false)
        // Aborted overrides the more=true, and settled being unset.
        doAbortedDeliveryTestImpl(true, null);
        // Check aborted=true, more=true, settled=false
        // Aborted overrides the more=true, and settled explicitly false.
        doAbortedDeliveryTestImpl(true, false);
    }

    private void doAbortedDeliveryTestImpl(boolean setMoreOnAbortedTransfer, Boolean setSettledOnAbortedTransfer) {
        MockTransportImpl transport = new MockTransportImpl();
        transport.setEmitFlowEventOnSend(false);
        Connection connection = Proton.connection();
        transport.bind(connection);

        connection.open();

        Session session = connection.session();
        session.open();

        String linkName1 = "myReceiver1";
        Receiver receiver1 = session.receiver(linkName1);
        receiver1.flow(3);
        receiver1.open();

        pumpMockTransport(transport);

        final UnsignedInteger r1handle = UnsignedInteger.ZERO;

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 4, transport.writes.size());

        // Give the necessary responses to open/begin/attach
        transport.handleFrame(new TransportFrame(0, new Open(), null));

        Begin begin = new Begin();
        begin.setRemoteChannel(UnsignedShort.valueOf((short) 0));
        begin.setNextOutgoingId(UnsignedInteger.ONE);
        begin.setIncomingWindow(UnsignedInteger.valueOf(1024));
        begin.setOutgoingWindow(UnsignedInteger.valueOf(1024));
        transport.handleFrame(new TransportFrame(0, begin, null));

        Attach attach1 = new Attach();
        attach1.setHandle(r1handle);
        attach1.setRole(Role.SENDER);
        attach1.setName(linkName1);
        attach1.setInitialDeliveryCount(UnsignedInteger.ZERO);
        transport.handleFrame(new TransportFrame(0, attach1, null));

        String deliveryTag1 = "tag1";
        String deliveryTag2 = "tag2";
        String deliveryTag3 = "tag3";

        // Receive first delivery
        handlePartialTransfer(transport, r1handle, UnsignedInteger.ZERO, deliveryTag1, new byte[]{ 1 }, true);
        assertEquals("Unexpected incoming bytes count", 1, session.getIncomingBytes());
        handlePartialTransfer(transport, r1handle, UnsignedInteger.ZERO, deliveryTag1, new byte[]{ 2 }, false);

        assertEquals("Unexpected queued count", 1, receiver1.getQueued());
        assertEquals("Unexpected incoming bytes count", 2, session.getIncomingBytes());
        assertEquals("Unexpected credit", 3, receiver1.getCredit());

        // Receive first transfer for a multi-frame delivery
        handlePartialTransfer(transport, r1handle, UnsignedInteger.ONE, deliveryTag2, new byte[]{ 3 }, true);
        assertEquals("Unexpected queued count", 2, receiver1.getQueued());
        assertEquals("Unexpected credit", 3, receiver1.getCredit());
        assertEquals("Unexpected incoming bytes count", 3, session.getIncomingBytes());
        // Receive second transfer for a multi-frame delivery, indicating it is aborted
        handlePartialTransfer(transport, r1handle, UnsignedInteger.ONE, deliveryTag2, new byte[]{ 4 }, setMoreOnAbortedTransfer, true, setSettledOnAbortedTransfer);
        assertEquals("Unexpected queued count", 2, receiver1.getQueued());
        assertEquals("Unexpected credit", 3, receiver1.getCredit());
        // The aborted frame payload, if any, is dropped. Earlier payload could have already been read, was
        // previously accounted for, and is incomplete, leaving alone for regular cleanup accounting handling.
        assertEquals("Unexpected incoming bytes count", 3, session.getIncomingBytes());

        // Receive transfers for ANOTHER delivery, expect it not to fail, since the earlier delivery aborted
        handlePartialTransfer(transport, r1handle, UnsignedInteger.valueOf(2), deliveryTag3, new byte[]{ 5 }, true);
        handlePartialTransfer(transport, r1handle, UnsignedInteger.valueOf(2), deliveryTag3, new byte[]{ 6 }, false);
        assertEquals("Unexpected queued count", 3, receiver1.getQueued());
        assertEquals("Unexpected credit", 3, receiver1.getCredit());
        assertEquals("Unexpected incoming bytes count", 5, session.getIncomingBytes());

        // Check the first delivery
        verifyDeliveryRawPayload(receiver1, deliveryTag1, new byte[] { 1, 2 });
        assertEquals("Unexpected queued count", 2, receiver1.getQueued());
        assertEquals("Unexpected credit", 2, receiver1.getCredit());
        assertEquals("Unexpected incoming bytes count", 3, session.getIncomingBytes());

        // Check the aborted delivery
        Delivery delivery = receiver1.current();
        assertTrue(Arrays.equals(deliveryTag2.getBytes(StandardCharsets.UTF_8), delivery.getTag()));

        assertTrue(delivery.isAborted());
        assertTrue(delivery.remotelySettled()); // Since aborted implicitly means it is settled.
        assertTrue(delivery.isPartial());
        assertTrue(delivery.isReadable());

        byte[] received = new byte[delivery.pending()];
        int len = receiver1.recv(received, 0, BUFFER_SIZE);
        assertEquals("unexpected length", len, received.length);

        assertArrayEquals("Received delivery payload not as expected", new byte[] { 3 }, received);

        assertTrue("receiver did not advance", receiver1.advance());

        assertEquals("Unexpected queued count", 1, receiver1.getQueued());
        assertEquals("Unexpected credit", 1, receiver1.getCredit());
        assertEquals("Unexpected incoming bytes count", 2, session.getIncomingBytes());

        // Check the third delivery
        verifyDeliveryRawPayload(receiver1, deliveryTag3, new byte[] { 5, 6 });
        assertEquals("Unexpected queued count", 0, receiver1.getQueued());
        assertEquals("Unexpected credit", 0, receiver1.getCredit());
        assertEquals("Unexpected incoming bytes count", 0, session.getIncomingBytes());

        // Flow new credit and check delivery-count + credit on wire are as expected.
        receiver1.flow(123);
        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 5, transport.writes.size());
        assertTrue("Unexpected frame type", transport.writes.get(4) instanceof Flow);
        Flow sentFlow = (Flow) transport.writes.get(4);

        assertEquals("Unexpected delivery count", UnsignedInteger.valueOf(3), sentFlow.getDeliveryCount());
        assertEquals("Unexpected credit", UnsignedInteger.valueOf(123), sentFlow.getLinkCredit());
    }

    @Test
    public void testErrorConditionDefault() {
        TransportImpl transport = new TransportImpl();
        assertNull("Expected null ErrorCondition given historic behaviour", transport.getCondition());
    }

    @Test
    public void testErrorConditionSetGet() {
        // Try setting with an empty condition object, expect to get a null back per historic behaviour.
        TransportImpl transport = new TransportImpl();

        ErrorCondition emptyErrorCondition = new ErrorCondition();
        assertNull("Expected empty Condition given historic behaviour", emptyErrorCondition.getCondition());
        transport.setCondition(emptyErrorCondition);
        assertNull("Expected null ErrorCondition given historic behaviour", transport.getCondition());

        // Try setting with a populated condition object.
        transport = new TransportImpl();

        Symbol condition = Symbol.getSymbol("some-error");
        String description = "some-error-description";
        ErrorCondition populatedErrorCondition = new ErrorCondition();
        populatedErrorCondition.setCondition(condition);
        populatedErrorCondition.setDescription(description);
        assertNotNull("Expected a Condition", populatedErrorCondition.getCondition());

        transport.setCondition(populatedErrorCondition);
        assertNotNull("Expected an ErrorCondition to be returned", transport.getCondition());
        assertEquals("Unexpected ErrorCondition returned", populatedErrorCondition, transport.getCondition());

        // Try setting again with another populated condition object.
        Symbol otherCondition = Symbol.getSymbol("some-other-error");
        String otherDescription = "some-other-error-description";
        ErrorCondition otherErrorCondition = new ErrorCondition();
        otherErrorCondition.setCondition(otherCondition);
        otherErrorCondition.setDescription(otherDescription);
        assertNotNull("Expected a Condition", otherErrorCondition.getCondition());

        assertNotEquals(condition, otherCondition);
        assertNotEquals(populatedErrorCondition.getCondition(), otherErrorCondition.getCondition());
        assertNotEquals(description, otherDescription);
        assertNotEquals(populatedErrorCondition.getDescription(), otherErrorCondition.getDescription());
        assertNotEquals(populatedErrorCondition, otherErrorCondition);

        transport.setCondition(otherErrorCondition);
        assertNotNull("Expected an ErrorCondition to be returned", transport.getCondition());
        assertEquals("Unexpected ErrorCondition returned", otherErrorCondition, transport.getCondition());

        // Try setting again with an empty condition object, expect to get a null back per historic behaviour.
        transport.setCondition(emptyErrorCondition);
        assertNull("Expected null ErrorCondition given historic behaviour", transport.getCondition());
    }

    @Test
    public void testErrorConditionAfterTransportClosed() {
        Symbol condition = Symbol.getSymbol("some-error");
        String description = "some-error-description";
        ErrorCondition origErrorCondition = new ErrorCondition();
        origErrorCondition.setCondition(condition);
        origErrorCondition.setDescription(description);
        assertNotNull("Expected a Condition", origErrorCondition.getCondition());

        // Set an error condition, then call 'closed' specifying an error.
        // Expect the original condition which was set to remain.
        TransportImpl transport = new TransportImpl();

        transport.setCondition(origErrorCondition);
        transport.closed(new TransportException("my-ignored-exception"));

        assertNotNull("Expected an ErrorCondition to be returned", transport.getCondition());
        assertEquals("Unexpected ErrorCondition returned", origErrorCondition, transport.getCondition());

        // ---------------------------------------------------------------- //

        // Set an error condition, then call 'closed' without an error.
        // Expect the original condition which was set to remain.
        transport = new TransportImpl();

        transport.setCondition(origErrorCondition);
        transport.closed(null);

        assertNotNull("Expected an ErrorCondition to be returned", transport.getCondition());
        assertEquals("Unexpected ErrorCondition returned", origErrorCondition, transport.getCondition());

        // ---------------------------------------------------------------- //

        // Without having set an error condition, call 'closed' specifying an error.
        // Expect a condition to be set.
        transport = new TransportImpl();
        transport.closed(new TransportException(description));

        assertNotNull("Expected an ErrorCondition to be returned", transport.getCondition());
        assertEquals("Unexpected condition returned", ConnectionError.FRAMING_ERROR, transport.getCondition().getCondition());
        assertEquals("Unexpected description returned", "org.apache.qpid.proton.engine.TransportException: " + description, transport.getCondition().getDescription());

        // ---------------------------------------------------------------- //

        // Without having set an error condition, call 'closed' without an error.
        // Expect a condition to be set.
        transport = new TransportImpl();

        transport.closed(null);

        assertNotNull("Expected an ErrorCondition to be returned", transport.getCondition());
        assertEquals("Unexpected ErrorCondition returned", ConnectionError.FRAMING_ERROR, transport.getCondition().getCondition());
        assertEquals("Unexpected description returned", "connection aborted", transport.getCondition().getDescription());
    }

    @Test
    public void testCloseFrameErrorAfterTransportClosed() {
        MockTransportImpl transport = new MockTransportImpl();
        Connection connection = Proton.connection();
        prepareAndOpenConnection(transport, connection);

        Symbol condition = Symbol.getSymbol("some-error");
        String description = "some-error-description";
        ErrorCondition origErrorCondition = new ErrorCondition();
        origErrorCondition.setCondition(condition);
        origErrorCondition.setDescription(description);
        assertNotNull("Expected a Condition", origErrorCondition.getCondition());

        // Set an error condition, then call 'closed' specifying an error.
        // Expect the original condition which was set to be emitted
        // in the close frame generated.

        transport.setCondition(origErrorCondition);
        transport.closed(new TransportException("my-ignored-exception"));

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 2, transport.writes.size());
        FrameBody frameBody = transport.writes.get(1);
        assertTrue("Unexpected frame type", frameBody instanceof Close);
        assertEquals("Unexpected condition", origErrorCondition, ((Close) frameBody).getError());

        // ---------------------------------------------------------------- //

        // Set an error condition, then call 'closed' without an error.
        // Expect the original condition which was set to be emitted
        // in the close frame generated.

        transport = new MockTransportImpl();
        connection = Proton.connection();
        prepareAndOpenConnection(transport, connection);

        transport.setCondition(origErrorCondition);
        transport.closed(null);

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 2, transport.writes.size());
        frameBody = transport.writes.get(1);
        assertTrue("Unexpected frame type", frameBody instanceof Close);
        assertEquals("Unexpected condition", origErrorCondition, ((Close) frameBody).getError());

        // ---------------------------------------------------------------- //

        // Without having set an error condition, call 'closed' specifying an error.
        // Expect a condition to be emitted in the close frame generated.
        transport = new MockTransportImpl();
        connection = Proton.connection();
        prepareAndOpenConnection(transport, connection);

        transport.closed(new TransportException(description));

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 2, transport.writes.size());
        frameBody = transport.writes.get(1);
        assertTrue("Unexpected frame type", frameBody instanceof Close);
        ErrorCondition expectedCondition = new ErrorCondition(ConnectionError.FRAMING_ERROR, "org.apache.qpid.proton.engine.TransportException: " + description);
        assertEquals("Unexpected condition", expectedCondition, ((Close) frameBody).getError());

        // ---------------------------------------------------------------- //

        // Without having set an error condition, call 'closed' without an error.
        // Expect a condition to be emitted in the close frame generated.
        transport = new MockTransportImpl();
        connection = Proton.connection();
        prepareAndOpenConnection(transport, connection);

        transport.closed(null);

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 2, transport.writes.size());
        frameBody = transport.writes.get(1);
        assertTrue("Unexpected frame type", frameBody instanceof Close);
        expectedCondition = new ErrorCondition(ConnectionError.FRAMING_ERROR, "connection aborted");
        assertEquals("Unexpected condition", expectedCondition, ((Close) frameBody).getError());

        // ---------------------------------------------------------------- //

        // Without having set an error condition on the transport, call 'closed' with an error,
        // but then also set a condition on the connection, and expect the connection error
        // condition to be emitted in the close frame generated.
        transport = new MockTransportImpl();
        connection = Proton.connection();
        prepareAndOpenConnection(transport, connection);

        transport.closed(new TransportException("some other transport exception"));
        connection.setCondition(origErrorCondition);

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 2, transport.writes.size());
        frameBody = transport.writes.get(1);
        assertTrue("Unexpected frame type", frameBody instanceof Close);
        assertEquals("Unexpected condition", origErrorCondition, ((Close) frameBody).getError());
    }

    @Test
    public void testCloseFrameErrorAfterDecodeError() {
        MockTransportImpl transport = new MockTransportImpl();
        Connection connection = Proton.connection();

        Collector collector = Collector.Factory.create();
        connection.collect(collector);

        transport.bind(connection);
        connection.open();

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 1, transport.writes.size());
        assertTrue("Unexpected frame type", transport.writes.get(0) instanceof Open);

        assertEvents(collector, Event.Type.CONNECTION_INIT, Event.Type.CONNECTION_BOUND, Event.Type.CONNECTION_LOCAL_OPEN, Event.Type.TRANSPORT);

        // Provide the response bytes for the header
        transport.tail().put(AmqpHeader.HEADER);
        transport.process();

        // Provide the bytes for Open, but omit the mandatory container-id to provoke a decode error.
        byte[] bytes = new byte[] {  0x00, 0x00, 0x00, 0x0C, // Frame size = 12 bytes.
                                     0x02, 0x00, 0x00, 0x00, // DOFF, TYPE, 2x CHANNEL
                                     0x00, 0x53, 0x10, 0x45};// Described-type, ulong type, open descriptor, list0.

        int capacity = transport.capacity();
        assertTrue("Unexpected transport capacity: " + capacity, capacity > bytes.length);

        transport.tail().put(bytes);
        transport.process();

        assertEvents(collector, Event.Type.TRANSPORT_ERROR, Event.Type.TRANSPORT_TAIL_CLOSED);

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 2, transport.writes.size());
        FrameBody frameBody = transport.writes.get(1);
        assertTrue("Unexpected frame type", frameBody instanceof Close);

        // Expect the close frame generated to contain a decode error condition referencing the missing container-id.
        ErrorCondition expectedCondition = new ErrorCondition();
        expectedCondition.setCondition(AmqpError.DECODE_ERROR);
        expectedCondition.setDescription("The container-id field cannot be omitted");

        assertEquals("Unexpected condition", expectedCondition, ((Close) frameBody).getError());
    }

    @Test
    public void testEmptyBeginProvokesDecodeError() {
        // Provide the bytes for Begin, but omit any fields to provoke a decode error.
        byte[] bytes = new byte[] {  0x00, 0x00, 0x00, 0x0C, // Frame size = 12 bytes.
                0x02, 0x00, 0x00, 0x00, // DOFF, TYPE, 2x CHANNEL
                0x00, 0x53, 0x11, 0x45};// Described-type, ulong type, Begin descriptor, list0.

        doInvalidBeginProvokesDecodeErrorTestImpl(bytes, "The outgoing-window field cannot be omitted");
    }

    @Test
    public void testTruncatedBeginProvokesDecodeError1() {
        // Provide the bytes for Begin, but only give a null (i-e not-present) for the remote-channel.
        byte[] bytes = new byte[] {  0x00, 0x00, 0x00, 0x0F, // Frame size = 15 bytes.
                0x02, 0x00, 0x00, 0x00, // DOFF, TYPE, 2x CHANNEL
                0x00, 0x53, 0x11, (byte) 0xC0, // Described-type, ulong type, Begin descriptor, list8.
                0x03, 0x01, 0x40 }; // size (3), count (1), remote-channel (null).

        doInvalidBeginProvokesDecodeErrorTestImpl(bytes, "The outgoing-window field cannot be omitted");
    }

    @Test
    public void testTruncatedBeginProvokesDecodeError2() {
        // Provide the bytes for Begin, but only give a [not-present remote-channel +] next-outgoing-id and incoming-window. Provokes a decode error as there must be 4 fields.
        byte[] bytes = new byte[] {  0x00, 0x00, 0x00, 0x11, // Frame size = 17 bytes.
                0x02, 0x00, 0x00, 0x00, // DOFF, TYPE, 2x CHANNEL
                0x00, 0x53, 0x11, (byte) 0xC0, // Described-type, ulong type, Begin descriptor, list8.
                0x05, 0x03, 0x40, 0x43, 0x43 }; // size (5), count (3), remote-channel (null), next-outgoing-id (uint0), incoming-window (uint0).

        doInvalidBeginProvokesDecodeErrorTestImpl(bytes, "The outgoing-window field cannot be omitted");
    }

    @Test
    public void testTruncatedBeginProvokesDecodeError3() {
        // Provide the bytes for Begin, but only give a [not-present remote-channel +] next-outgoing-id and incoming-window, and send not-present/null for outgoing-window. Provokes a decode error as must be present.
        byte[] bytes = new byte[] {  0x00, 0x00, 0x00, 0x12, // Frame size = 18 bytes.
                0x02, 0x00, 0x00, 0x00, // DOFF, TYPE, 2x CHANNEL
                0x00, 0x53, 0x11, (byte) 0xC0, // Described-type, ulong type, Begin descriptor, list8.
                0x06, 0x04, 0x40, 0x43, 0x43, 0x40 }; // size (5), count (4), remote-channel (null), next-outgoing-id (uint0), incoming-window (uint0), outgoing-window (null).

        doInvalidBeginProvokesDecodeErrorTestImpl(bytes, "Unexpected null value - mandatory field not set? (the outgoing-window field is mandatory)");
    }

    private void doInvalidBeginProvokesDecodeErrorTestImpl(byte[] bytes, String description) {
        MockTransportImpl transport = new MockTransportImpl();
        Connection connection = Proton.connection();

        Collector collector = Collector.Factory.create();
        connection.collect(collector);

        transport.bind(connection);
        connection.open();

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 1, transport.writes.size());
        assertTrue("Unexpected frame type", transport.writes.get(0) instanceof Open);

        // Provide the response bytes for the header
        transport.tail().put(AmqpHeader.HEADER);
        transport.process();

        // Send the necessary response to open
        transport.handleFrame(new TransportFrame(0, new Open(), null));

        int capacity = transport.capacity();
        assertTrue("Unexpected transport capacity: " + capacity, capacity > bytes.length);

        transport.tail().put(bytes);
        transport.process();

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 2, transport.writes.size());
        FrameBody frameBody = transport.writes.get(1);
        assertTrue("Unexpected frame type", frameBody instanceof Close);

        // Expect the close frame generated to contain a decode error condition referencing the missing container-id.
        ErrorCondition expectedCondition = new ErrorCondition();
        expectedCondition.setCondition(AmqpError.DECODE_ERROR);
        expectedCondition.setDescription(description);

        assertEquals("Unexpected condition", expectedCondition, ((Close) frameBody).getError());
    }

    @Test
    public void testEmptyFlowProvokesDecodeError() {
        // Provide the bytes for Flow, but omit any fields to provoke a decode error.
        byte[] bytes = new byte[] {  0x00, 0x00, 0x00, 0x0C, // Frame size = 12 bytes.
                0x02, 0x00, 0x00, 0x00, // DOFF, TYPE, 2x CHANNEL
                0x00, 0x53, 0x13, 0x45};// Described-type, ulong type, Flow descriptor, list0.

        doInvalidFlowProvokesDecodeErrorTestImpl(bytes, "The outgoing-window field cannot be omitted");
    }

    @Test
    public void testTruncatedFlowProvokesDecodeError1() {
        // Provide the bytes for Flow, but only give a 0 for the next-incoming-id. Provokes a decode error as there must be 4 fields.
        byte[] bytes = new byte[] {  0x00, 0x00, 0x00, 0x0F, // Frame size = 15 bytes.
                0x02, 0x00, 0x00, 0x00, // DOFF, TYPE, 2x CHANNEL
                0x00, 0x53, 0x13, (byte) 0xC0, // Described-type, ulong type, Flow descriptor, list8.
                0x03, 0x01, 0x43 }; // size (3), count (1), next-incoming-id (uint0).

        doInvalidFlowProvokesDecodeErrorTestImpl(bytes, "The outgoing-window field cannot be omitted");
    }

    @Test
    public void testTruncatedFlowProvokesDecodeError2() {
        // Provide the bytes for Flow, but only give a next-incoming-id and incoming-window and next-outgoing-id. Provokes a decode error as there must be 4 fields.
        byte[] bytes = new byte[] {  0x00, 0x00, 0x00, 0x11, // Frame size = 17 bytes.
                0x02, 0x00, 0x00, 0x00, // DOFF, TYPE, 2x CHANNEL
                0x00, 0x53, 0x13, (byte) 0xC0, // Described-type, ulong type, Flow descriptor, list8.
                0x05, 0x03, 0x43, 0x43, 0x43 }; // size (5), count (3), next-incoming-id (0), incoming-window (uint0), next-outgoing-id (uint0).

        doInvalidFlowProvokesDecodeErrorTestImpl(bytes, "The outgoing-window field cannot be omitted");
    }

    @Test
    public void testTruncatedFlowProvokesDecodeError3() {
        // Provide the bytes for Flow, but only give a next-incoming-id and incoming-window and next-outgoing-id, and send not-present/null for outgoing-window. Provokes a decode error as must be present.
        byte[] bytes = new byte[] {  0x00, 0x00, 0x00, 0x12, // Frame size = 18 bytes.
                0x02, 0x00, 0x00, 0x00, // DOFF, TYPE, 2x CHANNEL
                0x00, 0x53, 0x13, (byte) 0xC0, // Described-type, ulong type, Flow descriptor, list8.
                0x06, 0x04, 0x43, 0x43, 0x43, 0x40 }; // size (5), count (4), next-incoming-id (0), incoming-window (uint0), next-outgoing-id (uint0), outgoing-window (null).

        doInvalidFlowProvokesDecodeErrorTestImpl(bytes, "Unexpected null value - mandatory field not set? (the outgoing-window field is mandatory)");
    }

    private void doInvalidFlowProvokesDecodeErrorTestImpl(byte[] bytes, String description) {
        MockTransportImpl transport = new MockTransportImpl();
        Connection connection = Proton.connection();

        Collector collector = Collector.Factory.create();
        connection.collect(collector);

        transport.bind(connection);
        connection.open();

        Session session = connection.session();
        session.open();

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 2, transport.writes.size());
        assertTrue("Unexpected frame type", transport.writes.get(0) instanceof Open);
        assertTrue("Unexpected frame type", transport.writes.get(1) instanceof Begin);

        // Provide the response bytes for the header
        transport.tail().put(AmqpHeader.HEADER);
        transport.process();


        // Send the necessary response to Open/Begin
        transport.handleFrame(new TransportFrame(0, new Open(), null));

        Begin begin = new Begin();
        begin.setRemoteChannel(UnsignedShort.valueOf((short) 0));
        begin.setNextOutgoingId(UnsignedInteger.ONE);
        begin.setIncomingWindow(UnsignedInteger.valueOf(1024));
        begin.setOutgoingWindow(UnsignedInteger.valueOf(1024));
        transport.handleFrame(new TransportFrame(0, begin, null));

        int capacity = transport.capacity();
        assertTrue("Unexpected transport capacity: " + capacity, capacity > bytes.length);

        transport.tail().put(bytes);
        transport.process();

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 3, transport.writes.size());
        FrameBody frameBody = transport.writes.get(2);
        assertTrue("Unexpected frame type", frameBody instanceof Close);

        // Expect the close frame generated to contain a decode error condition referencing the missing container-id.
        ErrorCondition expectedCondition = new ErrorCondition();
        expectedCondition.setCondition(AmqpError.DECODE_ERROR);
        expectedCondition.setDescription(description);

        assertEquals("Unexpected condition", expectedCondition, ((Close) frameBody).getError());
    }

    @Test
    public void testEmptyTransferProvokesDecodeError() {
        // Provide the bytes for Transfer, but omit any fields to provoke a decode error.
        byte[] bytes = new byte[] {  0x00, 0x00, 0x00, 0x0C, // Frame size = 12 bytes.
                0x02, 0x00, 0x00, 0x00, // DOFF, TYPE, 2x CHANNEL
                0x00, 0x53, 0x14, 0x45};// Described-type, ulong type, Transfer descriptor, list0.

        doInvalidTransferProvokesDecodeErrorTestImpl(bytes, "The handle field cannot be omitted");
    }

    @Test
    public void testTruncatedTransferProvokesDecodeError() {
        // Provide the bytes for Transfer, but only give a null for the not-present handle. Provokes a decode error as there must be a handle.
        byte[] bytes = new byte[] {  0x00, 0x00, 0x00, 0x0F, // Frame size = 15 bytes.
                0x02, 0x00, 0x00, 0x00, // DOFF, TYPE, 2x CHANNEL
                0x00, 0x53, 0x14, (byte) 0xC0, // Described-type, ulong type, Transfer descriptor, list8.
                0x03, 0x01, 0x40 }; // size (3), count (1), handle (null / not-present).

        doInvalidTransferProvokesDecodeErrorTestImpl(bytes, "Unexpected null value - mandatory field not set? (the handle field is mandatory)");
    }

    private void doInvalidTransferProvokesDecodeErrorTestImpl(byte[] bytes, String description) {
        MockTransportImpl transport = new MockTransportImpl();
        Connection connection = Proton.connection();

        Collector collector = Collector.Factory.create();
        connection.collect(collector);

        transport.bind(connection);
        connection.open();

        Session session = connection.session();
        session.open();

        String linkName = "myReceiver";
        Receiver receiver = session.receiver(linkName);
        receiver.flow(5);
        receiver.open();

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 4, transport.writes.size());
        assertTrue("Unexpected frame type", transport.writes.get(0) instanceof Open);
        assertTrue("Unexpected frame type", transport.writes.get(1) instanceof Begin);
        assertTrue("Unexpected frame type", transport.writes.get(2) instanceof Attach);
        assertTrue("Unexpected frame type", transport.writes.get(3) instanceof Flow);

        // Provide the response bytes for the header
        transport.tail().put(AmqpHeader.HEADER);
        transport.process();

        // Send the necessary response to Open/Begin/Attach
        transport.handleFrame(new TransportFrame(0, new Open(), null));

        Begin begin = new Begin();
        begin.setRemoteChannel(UnsignedShort.valueOf((short) 0));
        begin.setNextOutgoingId(UnsignedInteger.ONE);
        begin.setIncomingWindow(UnsignedInteger.valueOf(1024));
        begin.setOutgoingWindow(UnsignedInteger.valueOf(1024));
        transport.handleFrame(new TransportFrame(0, begin, null));

        Attach attach = new Attach();
        attach.setHandle(UnsignedInteger.ZERO);
        attach.setRole(Role.SENDER);
        attach.setName(linkName);
        attach.setInitialDeliveryCount(UnsignedInteger.ZERO);
        transport.handleFrame(new TransportFrame(0, attach, null));

        int capacity = transport.capacity();
        assertTrue("Unexpected transport capacity: " + capacity, capacity > bytes.length);

        transport.tail().put(bytes);
        transport.process();

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 5, transport.writes.size());
        FrameBody frameBody = transport.writes.get(4);
        assertTrue("Unexpected frame type", frameBody instanceof Close);

        // Expect the close frame generated to contain a decode error condition referencing the missing container-id.
        ErrorCondition expectedCondition = new ErrorCondition();
        expectedCondition.setCondition(AmqpError.DECODE_ERROR);
        expectedCondition.setDescription(description);

        assertEquals("Unexpected condition", expectedCondition, ((Close) frameBody).getError());
    }

    @Test
    public void testEmptyDispositionProvokesDecodeError() {
        // Provide the bytes for Disposition, but omit any fields to provoke a decode error.
        byte[] bytes = new byte[] {  0x00, 0x00, 0x00, 0x0C, // Frame size = 12 bytes.
                0x02, 0x00, 0x00, 0x00, // DOFF, TYPE, 2x CHANNEL
                0x00, 0x53, 0x15, 0x45};// Described-type, ulong type, Disposition descriptor, list0.

        doInvalidDispositionProvokesDecodeErrorTestImpl(bytes, "The 'first' field cannot be omitted");
    }

    @Test
    public void testTruncatedDispositionProvokesDecodeError() {
        // Provide the bytes for Disposition, but only give a null/not-present for the 'first' field. Provokes a decode error as there must be a role and 'first'.
        byte[] bytes = new byte[] {  0x00, 0x00, 0x00, 0x10, // Frame size = 16 bytes.
                0x02, 0x00, 0x00, 0x00, // DOFF, TYPE, 2x CHANNEL
                0x00, 0x53, 0x15, (byte) 0xC0, // Described-type, ulong type, Disposition descriptor, list8.
                0x04, 0x02, 0x41, 0x40 }; // size (4), count (2), role (receiver - the peers perspective), first ( null / not-present)

        doInvalidDispositionProvokesDecodeErrorTestImpl(bytes, "Unexpected null value - mandatory field not set? (the first field is mandatory)");
    }

    private void doInvalidDispositionProvokesDecodeErrorTestImpl(byte[] bytes, String description) {
        MockTransportImpl transport = new MockTransportImpl();
        Connection connection = Proton.connection();

        Collector collector = Collector.Factory.create();
        connection.collect(collector);

        transport.bind(connection);
        connection.open();

        Session session = connection.session();
        session.open();

        String linkName = "mySender";
        Sender sender = session.sender(linkName);
        sender.open();

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 3, transport.writes.size());
        assertTrue("Unexpected frame type", transport.writes.get(0) instanceof Open);
        assertTrue("Unexpected frame type", transport.writes.get(1) instanceof Begin);
        assertTrue("Unexpected frame type", transport.writes.get(2) instanceof Attach);

        // Provide the response bytes for the header
        transport.tail().put(AmqpHeader.HEADER);
        transport.process();

        // Send the necessary response to Open/Begin/Attach plus some credit
        transport.handleFrame(new TransportFrame(0, new Open(), null));

        Begin begin = new Begin();
        begin.setRemoteChannel(UnsignedShort.valueOf((short) 0));
        begin.setNextOutgoingId(UnsignedInteger.ONE);
        begin.setIncomingWindow(UnsignedInteger.valueOf(1024));
        begin.setOutgoingWindow(UnsignedInteger.valueOf(1024));
        transport.handleFrame(new TransportFrame(0, begin, null));

        Attach attach = new Attach();
        attach.setHandle(UnsignedInteger.ZERO);
        attach.setRole(Role.SENDER);
        attach.setName(linkName);
        attach.setInitialDeliveryCount(UnsignedInteger.ZERO);
        transport.handleFrame(new TransportFrame(0, attach, null));

        int credit = 1;
        Flow flow = new Flow();
        flow.setHandle(UnsignedInteger.ZERO);
        flow.setDeliveryCount(UnsignedInteger.ZERO);
        flow.setNextIncomingId(UnsignedInteger.ONE);
        flow.setNextOutgoingId(UnsignedInteger.ZERO);
        flow.setIncomingWindow(UnsignedInteger.valueOf(1024));
        flow.setOutgoingWindow(UnsignedInteger.valueOf(1024));
        flow.setDrain(true);
        flow.setLinkCredit(UnsignedInteger.valueOf(credit));
        transport.handleFrame(new TransportFrame(0, flow, null));

        sendMessage(sender, "tag1", "content1");

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 4, transport.writes.size());
        assertTrue("Unexpected frame type", transport.writes.get(3) instanceof Transfer);

        int capacity = transport.capacity();
        assertTrue("Unexpected transport capacity: " + capacity, capacity > bytes.length);

        transport.tail().put(bytes);
        transport.process();

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 5, transport.writes.size());
        FrameBody frameBody = transport.writes.get(4);
        assertTrue("Unexpected frame type", frameBody instanceof Close);

        // Expect the close frame generated to contain a decode error condition referencing the missing container-id.
        ErrorCondition expectedCondition = new ErrorCondition();
        expectedCondition.setCondition(AmqpError.DECODE_ERROR);
        expectedCondition.setDescription(description);

        assertEquals("Unexpected condition", expectedCondition, ((Close) frameBody).getError());
    }

    private void prepareAndOpenConnection(MockTransportImpl transport, Connection connection) {
        transport.bind(connection);
        connection.open();

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 1, transport.writes.size());
        assertTrue("Unexpected frame type", transport.writes.get(0) instanceof Open);

        // Give the necessary response to open
        transport.handleFrame(new TransportFrame(0, new Open(), null));
    }

    @Test
    public void testProtocolTracingLogsFrameToTracer()
    {
        Connection connection = new ConnectionImpl();
        List<TransportFrame> frames = new ArrayList<>();
        _transport.setProtocolTracer(new ProtocolTracer()
        {
            @Override
            public void receivedFrame(final TransportFrame transportFrame)
            {
                frames.add(transportFrame);
            }

            @Override
            public void sentFrame(TransportFrame transportFrame) { }
        });

        assertTrue(_transport.isHandlingFrames());
        _transport.bind(connection);

        assertTrue(_transport.isHandlingFrames());
        _transport.handleFrame(TRANSPORT_FRAME_OPEN);
        assertTrue(_transport.isHandlingFrames());

        assertEquals(1, frames.size());
        TransportFrame transportFrame = frames.get(0);
        assertTrue(transportFrame.getBody() instanceof Open);
        assertEquals(CHANNEL_ID, transportFrame.getChannel());
    }

    @Test
    public void testProtocolTracingLogsFrameToSystem()
    {
        Connection connection = new ConnectionImpl();
        TransportImpl spy = spy(_transport);

        assertTrue(spy.isHandlingFrames());
        spy.bind(connection);

        assertTrue(spy.isHandlingFrames());
        spy.handleFrame(TRANSPORT_FRAME_OPEN);
        assertTrue(spy.isHandlingFrames());

        ArgumentCaptor<TransportFrame> frameCatcher = ArgumentCaptor.forClass(TransportFrame.class);
        Mockito.verify(spy).log(eq(TransportImpl.INCOMING), frameCatcher.capture());

        assertEquals(TRANSPORT_FRAME_OPEN.getChannel(), frameCatcher.getValue().getChannel());
        assertTrue(frameCatcher.getValue().getBody() instanceof Open);
        assertNull(frameCatcher.getValue().getPayload());
    }

    @Test
    public void testProtocolTracingLogsHeaderToTracer()
    {
        doProtocolTracingLogsHeaderToTracerTestImpl(false);
    }

    @Test
    public void testProtocolTracingLogsHeaderSaslToTracer()
    {
        doProtocolTracingLogsHeaderToTracerTestImpl(true);
    }

    private void doProtocolTracingLogsHeaderToTracerTestImpl(boolean sasl)
    {
        Connection connection = new ConnectionImpl();
        AtomicReference<String> headerRef = new AtomicReference<>();
        _transport.setProtocolTracer(new ProtocolTracer()
        {
            @Override
            public void receivedHeader(String header)
            {
                assertTrue(headerRef.compareAndSet(null, header));
            }

            @Override
            public void receivedFrame(TransportFrame transportFrame) { }
            @Override
            public void sentFrame(TransportFrame transportFrame) { }

        });

        if (sasl)
        {
            _transport.sasl();
        }

        assertTrue(_transport.isHandlingFrames());
        _transport.bind(connection);

        assertTrue(_transport.isHandlingFrames());
        _transport.getInputBuffer().put(sasl ? AmqpHeader.SASL_HEADER : AmqpHeader.HEADER);
        _transport.process();
        assertTrue(_transport.isHandlingFrames());

        assertNotNull(headerRef.get());
        assertEquals(sasl ? "SASL" : "AMQP", headerRef.get());
    }

    @Test
    public void testProtocolTracingLogsHeaderToSystem()
    {
        doProtocolTracingLogsHeaderToSystemTestImpl(false);
    }

    @Test
    public void testProtocolTracingLogsHeaderSaslToSystem()
    {
        doProtocolTracingLogsHeaderToSystemTestImpl(true);
    }

    private void doProtocolTracingLogsHeaderToSystemTestImpl(boolean sasl)
    {
        Connection connection = new ConnectionImpl();

        AtomicReference<String> headerRef = new AtomicReference<>();
        AtomicReference<String> eventRef = new AtomicReference<>();
        TransportImpl transport = new TransportImpl()
        {
            @Override
            public void log(String event, String header) {
                assertTrue(eventRef.compareAndSet(null, event));
                assertTrue(headerRef.compareAndSet(null, header));
            }
        };
        transport.trace(2);

        if (sasl)
        {
            transport.sasl();
        }

        transport.bind(connection);

        transport.getInputBuffer().put(sasl ? AmqpHeader.SASL_HEADER : AmqpHeader.HEADER);
        transport.process();

        assertEquals(TransportImpl.INCOMING, eventRef.get());
        assertEquals(sasl ? "SASL" : "AMQP", headerRef.get());
    }

    @Test
    public void testProtocolTracingLogsOutboundHeaderToTracer()
    {
        doProtocolTracingLogsOutboundHeaderToTracerTestImpl(false);
    }

    @Test
    public void testProtocolTracingLogsOutboundHeaderSaslToTracer()
    {
        doProtocolTracingLogsOutboundHeaderToTracerTestImpl(true);
    }

    private void doProtocolTracingLogsOutboundHeaderToTracerTestImpl(boolean sasl)
    {
        Connection connection = new ConnectionImpl();
        AtomicReference<String> headerRef = new AtomicReference<>();
        _transport.setProtocolTracer(new ProtocolTracer()
        {
            @Override
            public void sentHeader(String header)
            {
                assertTrue(headerRef.compareAndSet(null, header));
            }

            @Override
            public void receivedFrame(TransportFrame transportFrame) { }
            @Override
            public void sentFrame(TransportFrame transportFrame) { }

        });

        if (sasl)
        {
            _transport.sasl();
        }

        _transport.bind(connection);

        ByteBuffer expected = ByteBuffer.wrap(sasl ? AmqpHeader.SASL_HEADER : AmqpHeader.HEADER);

        _transport.pending();
        assertEquals(expected, _transport.getOutputBuffer());

        assertEquals(sasl ? "SASL" : "AMQP", headerRef.get());
    }

    @Test
    public void testProtocolTracingLogsOutboundHeaderToSystem()
    {
        doProtocolTracingLogsOutboundHeaderToSystemTestImpl(false);
    }

    @Test
    public void testProtocolTracingLogsOutboundHeaderSaslToSystem()
    {
        doProtocolTracingLogsOutboundHeaderToSystemTestImpl(true);
    }

    private void doProtocolTracingLogsOutboundHeaderToSystemTestImpl(boolean sasl)
    {
        Connection connection = new ConnectionImpl();

        AtomicReference<String> headerRef = new AtomicReference<>();
        AtomicReference<String> eventRef = new AtomicReference<>();
        TransportImpl transport = new TransportImpl()
        {
            @Override
            public void log(String event, String header)
            {
                assertTrue(eventRef.compareAndSet(null, event));
                assertTrue(headerRef.compareAndSet(null, header));
            }
        };
        transport.trace(2);

        if (sasl)
        {
            transport.sasl();
        }

        transport.bind(connection);

        ByteBuffer expected = ByteBuffer.wrap(sasl ? AmqpHeader.SASL_HEADER : AmqpHeader.HEADER);

        transport.pending();
        assertEquals(expected, transport.getOutputBuffer());

        assertEquals(TransportImpl.OUTGOING, eventRef.get());
        assertEquals(sasl ? "SASL" : "AMQP", headerRef.get());
    }

    /**
     * Verify that no Disposition frame is emitted by the Transport should a Delivery
     * have disposition applied after the delivery has been settled previously.
     */
    @Test
    public void testNoDispositionUpdatesAfterSettlementProceessedSender()
    {
        MockTransportImpl transport = new MockTransportImpl();
        Connection connection = Proton.connection();
        transport.bind(connection);

        connection.open();

        Session session = connection.session();
        session.open();

        String linkName = "myReceiver";
        Receiver receiver = session.receiver(linkName);
        receiver.flow(5);
        receiver.open();

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 4, transport.writes.size());

        assertTrue("Unexpected frame type", transport.writes.get(0) instanceof Open);
        assertTrue("Unexpected frame type", transport.writes.get(1) instanceof Begin);
        assertTrue("Unexpected frame type", transport.writes.get(2) instanceof Attach);
        assertTrue("Unexpected frame type", transport.writes.get(3) instanceof Flow);

        Delivery delivery = receiver.current();
        assertNull("Should not yet have a delivery", delivery);

        // Send the necessary responses to open/begin/attach as well as a transfer
        transport.handleFrame(new TransportFrame(0, new Open(), null));

        Begin begin = new Begin();
        begin.setRemoteChannel(UnsignedShort.valueOf((short) 0));
        begin.setNextOutgoingId(UnsignedInteger.ONE);
        begin.setIncomingWindow(UnsignedInteger.valueOf(1024));
        begin.setOutgoingWindow(UnsignedInteger.valueOf(1024));
        transport.handleFrame(new TransportFrame(0, begin, null));

        Attach attach = new Attach();
        attach.setHandle(UnsignedInteger.ZERO);
        attach.setRole(Role.SENDER);
        attach.setName(linkName);
        attach.setInitialDeliveryCount(UnsignedInteger.ZERO);
        transport.handleFrame(new TransportFrame(0, attach, null));

        String deliveryTag = "tag1";
        String messageContent = "content1";
        handleTransfer(transport, 1, deliveryTag, messageContent);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 4, transport.writes.size());

        delivery = verifyDelivery(receiver, deliveryTag, messageContent);
        assertNotNull("Should now have a delivery", delivery);

        delivery.disposition(Accepted.getInstance());
        delivery.settle();

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 5, transport.writes.size());
        assertTrue("Unexpected frame type", transport.writes.get(4) instanceof Disposition);

        // Should not produce any new frames being written
        delivery.disposition(Accepted.getInstance());

        connection.close();
        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 6, transport.writes.size());
        assertTrue("Unexpected frame type", transport.writes.get(5) instanceof Close);
    }

    /**
     * Verify that no Disposition frame is emitted by the Transport should a Delivery
     * have disposition applied after the delivery has been settled previously.
     */
    @Test
    public void testNoDispositionUpdatesAfterSettlementProceessedReceiver()
    {
        MockTransportImpl transport = new MockTransportImpl();
        Connection connection = Proton.connection();
        transport.bind(connection);

        connection.open();

        Session session = connection.session();
        session.open();

        String linkName = "myReceiver";
        Sender sender = session.sender(linkName);
        sender.open();

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 3, transport.writes.size());

        assertTrue("Unexpected frame type", transport.writes.get(0) instanceof Open);
        assertTrue("Unexpected frame type", transport.writes.get(1) instanceof Begin);
        assertTrue("Unexpected frame type", transport.writes.get(2) instanceof Attach);

        // Send the necessary responses to open/begin/attach as well as a transfer
        transport.handleFrame(new TransportFrame(0, new Open(), null));

        Begin begin = new Begin();
        begin.setRemoteChannel(UnsignedShort.valueOf((short) 0));
        begin.setNextOutgoingId(UnsignedInteger.ONE);
        begin.setIncomingWindow(UnsignedInteger.valueOf(1024));
        begin.setOutgoingWindow(UnsignedInteger.valueOf(1024));
        transport.handleFrame(new TransportFrame(0, begin, null));

        Attach attach = new Attach();
        attach.setHandle(UnsignedInteger.ZERO);
        attach.setRole(Role.RECEIVER);
        attach.setName(linkName);
        attach.setInitialDeliveryCount(UnsignedInteger.ZERO);
        transport.handleFrame(new TransportFrame(0, attach, null));

        int credit = 1;
        Flow flow = new Flow();
        flow.setHandle(UnsignedInteger.ZERO);
        flow.setDeliveryCount(UnsignedInteger.ZERO);
        flow.setNextIncomingId(UnsignedInteger.ONE);
        flow.setNextOutgoingId(UnsignedInteger.ZERO);
        flow.setIncomingWindow(UnsignedInteger.valueOf(1024));
        flow.setOutgoingWindow(UnsignedInteger.valueOf(1024));
        flow.setDrain(true);
        flow.setLinkCredit(UnsignedInteger.valueOf(credit));
        transport.handleFrame(new TransportFrame(0, flow, null));

        Delivery delivery = sendMessage(sender, "tag1", "content1");

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 4, transport.writes.size());
        assertTrue("Unexpected frame type", transport.writes.get(3) instanceof Transfer);

        delivery.disposition(Accepted.getInstance());
        delivery.settle();

        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 5, transport.writes.size());
        assertTrue("Unexpected frame type", transport.writes.get(4) instanceof Disposition);

        // Should not produce any new frames being written
        delivery.disposition(Accepted.getInstance());

        connection.close();
        pumpMockTransport(transport);

        assertEquals("Unexpected frames written: " + getFrameTypesWritten(transport), 6, transport.writes.size());
        assertTrue("Unexpected frame type", transport.writes.get(5) instanceof Close);
    }
}