/*
 * Copyright 2014 The Netty Project
 *
 * The Netty Project 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 io.netty.handler.codec.http2;

import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.ChannelPromise;
import io.netty.channel.DefaultEventLoopGroup;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.local.LocalAddress;
import io.netty.channel.local.LocalChannel;
import io.netty.channel.local.LocalServerChannel;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpMessage;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpObject;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http2.Http2TestUtil.Http2Runnable;
import io.netty.util.AsciiString;
import io.netty.util.CharsetUtil;
import io.netty.util.concurrent.Future;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.List;
import java.util.concurrent.CountDownLatch;

import static io.netty.handler.codec.http2.Http2CodecUtil.getEmbeddedHttp2Exception;
import static io.netty.handler.codec.http2.Http2Exception.isStreamError;
import static io.netty.handler.codec.http2.Http2TestUtil.of;
import static io.netty.handler.codec.http2.Http2TestUtil.runInChannel;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

/**
 * Testing the {@link InboundHttp2ToHttpPriorityAdapter} and base class {@link InboundHttp2ToHttpAdapter} for HTTP/2
 * frames into {@link HttpObject}s
 */
public class InboundHttp2ToHttpAdapterTest {
    private List<FullHttpMessage> capturedRequests;
    private List<FullHttpMessage> capturedResponses;

    @Mock
    private HttpResponseListener serverListener;

    @Mock
    private HttpResponseListener clientListener;

    @Mock
    private HttpSettingsListener settingsListener;

    private Http2ConnectionHandler serverHandler;
    private Http2ConnectionHandler clientHandler;
    private ServerBootstrap sb;
    private Bootstrap cb;
    private Channel serverChannel;
    private volatile Channel serverConnectedChannel;
    private Channel clientChannel;
    private CountDownLatch serverLatch;
    private CountDownLatch clientLatch;
    private CountDownLatch serverLatch2;
    private CountDownLatch clientLatch2;
    private CountDownLatch settingsLatch;
    private int maxContentLength;
    private HttpResponseDelegator serverDelegator;
    private HttpResponseDelegator clientDelegator;
    private HttpSettingsDelegator settingsDelegator;
    private Http2Exception clientException;

    @Before
    public void setup() throws Exception {
        MockitoAnnotations.initMocks(this);
    }

    @After
    public void teardown() throws Exception {
        cleanupCapturedRequests();
        cleanupCapturedResponses();
        if (clientChannel != null) {
            clientChannel.close().sync();
            clientChannel = null;
        }
        if (serverChannel != null) {
            serverChannel.close().sync();
            serverChannel = null;
        }
        if (serverConnectedChannel != null) {
            serverConnectedChannel.close().sync();
            serverConnectedChannel = null;
        }
        Future<?> serverGroup = sb.config().group().shutdownGracefully(0, 0, MILLISECONDS);
        Future<?> serverChildGroup = sb.config().childGroup().shutdownGracefully(0, 0, MILLISECONDS);
        Future<?> clientGroup = cb.config().group().shutdownGracefully(0, 0, MILLISECONDS);
        serverGroup.sync();
        serverChildGroup.sync();
        clientGroup.sync();
    }

    @Test
    public void clientRequestSingleHeaderNoDataFrames() throws Exception {
        boostrapEnv(1, 1, 1);
        final FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET,
                "/some/path/resource2", true);
        try {
            HttpHeaders httpHeaders = request.headers();
            httpHeaders.set(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), "https");
            httpHeaders.set(HttpHeaderNames.HOST, "example.org");
            httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
            httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, 0);
            httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16);
            final Http2Headers http2Headers = new DefaultHttp2Headers().method(new AsciiString("GET")).
                    scheme(new AsciiString("https")).authority(new AsciiString("example.org"))
                    .path(new AsciiString("/some/path/resource2"));
            runInChannel(clientChannel, new Http2Runnable() {
                @Override
                public void run() {
                    clientHandler.encoder().writeHeaders(ctxClient(), 3, http2Headers, 0, true, newPromiseClient());
                    clientChannel.flush();
                }
            });
            awaitRequests();
            ArgumentCaptor<FullHttpMessage> requestCaptor = ArgumentCaptor.forClass(FullHttpMessage.class);
            verify(serverListener).messageReceived(requestCaptor.capture());
            capturedRequests = requestCaptor.getAllValues();
            assertEquals(request, capturedRequests.get(0));
        } finally {
            request.release();
        }
    }

    @Test
    public void clientRequestSingleHeaderCookieSplitIntoMultipleEntries() throws Exception {
        boostrapEnv(1, 1, 1);
        final FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET,
                "/some/path/resource2", true);
        try {
            HttpHeaders httpHeaders = request.headers();
            httpHeaders.set(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), "https");
            httpHeaders.set(HttpHeaderNames.HOST, "example.org");
            httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
            httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, 0);
            httpHeaders.set(HttpHeaderNames.COOKIE, "a=b; c=d; e=f");
            httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16);
            final Http2Headers http2Headers = new DefaultHttp2Headers().method(new AsciiString("GET")).
                    scheme(new AsciiString("https")).authority(new AsciiString("example.org"))
                    .path(new AsciiString("/some/path/resource2"))
                    .add(HttpHeaderNames.COOKIE, "a=b")
                    .add(HttpHeaderNames.COOKIE, "c=d")
                    .add(HttpHeaderNames.COOKIE, "e=f");
            runInChannel(clientChannel, new Http2Runnable() {
                @Override
                public void run() {
                    clientHandler.encoder().writeHeaders(ctxClient(), 3, http2Headers, 0, true, newPromiseClient());
                    clientChannel.flush();
                }
            });
            awaitRequests();
            ArgumentCaptor<FullHttpMessage> requestCaptor = ArgumentCaptor.forClass(FullHttpMessage.class);
            verify(serverListener).messageReceived(requestCaptor.capture());
            capturedRequests = requestCaptor.getAllValues();
            assertEquals(request, capturedRequests.get(0));
        } finally {
            request.release();
        }
    }

    @Test
    public void clientRequestSingleHeaderCookieSplitIntoMultipleEntries2() throws Exception {
        boostrapEnv(1, 1, 1);
        final FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET,
                "/some/path/resource2", true);
        try {
            HttpHeaders httpHeaders = request.headers();
            httpHeaders.set(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), "https");
            httpHeaders.set(HttpHeaderNames.HOST, "example.org");
            httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
            httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, 0);
            httpHeaders.set(HttpHeaderNames.COOKIE, "a=b; c=d; e=f");
            httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16);
            final Http2Headers http2Headers = new DefaultHttp2Headers().method(new AsciiString("GET")).
                    scheme(new AsciiString("https")).authority(new AsciiString("example.org"))
                    .path(new AsciiString("/some/path/resource2"))
                    .add(HttpHeaderNames.COOKIE, "a=b; c=d")
                    .add(HttpHeaderNames.COOKIE, "e=f");
            runInChannel(clientChannel, new Http2Runnable() {
                @Override
                public void run() {
                    clientHandler.encoder().writeHeaders(ctxClient(), 3, http2Headers, 0, true, newPromiseClient());
                    clientChannel.flush();
                }
            });
            awaitRequests();
            ArgumentCaptor<FullHttpMessage> requestCaptor = ArgumentCaptor.forClass(FullHttpMessage.class);
            verify(serverListener).messageReceived(requestCaptor.capture());
            capturedRequests = requestCaptor.getAllValues();
            assertEquals(request, capturedRequests.get(0));
        } finally {
            request.release();
        }
    }

    @Test
    public void clientRequestSingleHeaderNonAsciiShouldThrow() throws Exception {
        boostrapEnv(1, 1, 1);
        final Http2Headers http2Headers = new DefaultHttp2Headers()
                .method(new AsciiString("GET"))
                .scheme(new AsciiString("https"))
                .authority(new AsciiString("example.org"))
                .path(new AsciiString("/some/path/resource2"))
                .add(new AsciiString("çã".getBytes(CharsetUtil.UTF_8)),
                        new AsciiString("Ãã".getBytes(CharsetUtil.UTF_8)));
        runInChannel(clientChannel, new Http2Runnable() {
            @Override
            public void run() {
                clientHandler.encoder().writeHeaders(ctxClient(), 3, http2Headers, 0, true, newPromiseClient());
                clientChannel.flush();
            }
        });
        awaitResponses();
        assertTrue(isStreamError(clientException));
    }

    @Test
    public void clientRequestOneDataFrame() throws Exception {
        boostrapEnv(1, 1, 1);
        final String text = "hello world";
        final ByteBuf content = Unpooled.copiedBuffer(text.getBytes());
        final FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET,
                "/some/path/resource2", content, true);
        try {
            HttpHeaders httpHeaders = request.headers();
            httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
            httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length());
            httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16);
            final Http2Headers http2Headers = new DefaultHttp2Headers().method(new AsciiString("GET")).path(
                    new AsciiString("/some/path/resource2"));
            runInChannel(clientChannel, new Http2Runnable() {
                @Override
                public void run() {
                    clientHandler.encoder().writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient());
                    clientHandler.encoder().writeData(ctxClient(), 3, content.retainedDuplicate(), 0, true,
                                                      newPromiseClient());
                    clientChannel.flush();
                }
            });
            awaitRequests();
            ArgumentCaptor<FullHttpMessage> requestCaptor = ArgumentCaptor.forClass(FullHttpMessage.class);
            verify(serverListener).messageReceived(requestCaptor.capture());
            capturedRequests = requestCaptor.getAllValues();
            assertEquals(request, capturedRequests.get(0));
        } finally {
            request.release();
        }
    }

    @Test
    public void clientRequestMultipleDataFrames() throws Exception {
        boostrapEnv(1, 1, 1);
        final String text = "hello world big time data!";
        final ByteBuf content = Unpooled.copiedBuffer(text.getBytes());
        final FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET,
                "/some/path/resource2", content, true);
        try {
            HttpHeaders httpHeaders = request.headers();
            httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
            httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length());
            httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16);
            final Http2Headers http2Headers = new DefaultHttp2Headers().method(new AsciiString("GET")).path(
                    new AsciiString("/some/path/resource2"));
            final int midPoint = text.length() / 2;
            runInChannel(clientChannel, new Http2Runnable() {
                @Override
                public void run() {
                    clientHandler.encoder().writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient());
                    clientHandler.encoder().writeData(
                            ctxClient(), 3, content.retainedSlice(0, midPoint), 0, false, newPromiseClient());
                    clientHandler.encoder().writeData(
                            ctxClient(), 3, content.retainedSlice(midPoint, text.length() - midPoint),
                            0, true, newPromiseClient());
                    clientChannel.flush();
                }
            });
            awaitRequests();
            ArgumentCaptor<FullHttpMessage> requestCaptor = ArgumentCaptor.forClass(FullHttpMessage.class);
            verify(serverListener).messageReceived(requestCaptor.capture());
            capturedRequests = requestCaptor.getAllValues();
            assertEquals(request, capturedRequests.get(0));
        } finally {
            request.release();
        }
    }

    @Test
    public void clientRequestMultipleEmptyDataFrames() throws Exception {
        boostrapEnv(1, 1, 1);
        final String text = "";
        final ByteBuf content = Unpooled.copiedBuffer(text.getBytes());
        final FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET,
                "/some/path/resource2", content, true);
        try {
            HttpHeaders httpHeaders = request.headers();
            httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
            httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length());
            httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16);
            final Http2Headers http2Headers = new DefaultHttp2Headers().method(new AsciiString("GET")).path(
                    new AsciiString("/some/path/resource2"));
            runInChannel(clientChannel, new Http2Runnable() {
                @Override
                public void run() {
                    clientHandler.encoder().writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient());
                    clientHandler.encoder().writeData(ctxClient(), 3, content.retain(), 0, false, newPromiseClient());
                    clientHandler.encoder().writeData(ctxClient(), 3, content.retain(), 0, false, newPromiseClient());
                    clientHandler.encoder().writeData(ctxClient(), 3, content.retain(), 0, true, newPromiseClient());
                    clientChannel.flush();
                }
            });
            awaitRequests();
            ArgumentCaptor<FullHttpMessage> requestCaptor = ArgumentCaptor.forClass(FullHttpMessage.class);
            verify(serverListener).messageReceived(requestCaptor.capture());
            capturedRequests = requestCaptor.getAllValues();
            assertEquals(request, capturedRequests.get(0));
        } finally {
            request.release();
        }
    }

    @Test
    public void clientRequestMultipleHeaders() throws Exception {
        boostrapEnv(1, 1, 1);
        // writeHeaders will implicitly add an END_HEADERS tag each time and so this test does not follow the HTTP
        // message flow. We currently accept this message flow and just add the second headers to the trailing headers.
        final String text = "";
        final ByteBuf content = Unpooled.copiedBuffer(text.getBytes());
        final FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET,
                "/some/path/resource2", content, true);
        try {
            HttpHeaders httpHeaders = request.headers();
            httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
            httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length());
            httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16);
            HttpHeaders trailingHeaders = request.trailingHeaders();
            trailingHeaders.set(of("FoO"), of("goo"));
            trailingHeaders.set(of("foO2"), of("goo2"));
            trailingHeaders.add(of("fOo2"), of("goo3"));
            final Http2Headers http2Headers = new DefaultHttp2Headers().method(new AsciiString("GET")).path(
                    new AsciiString("/some/path/resource2"));
            final Http2Headers http2Headers2 = new DefaultHttp2Headers()
                    .set(new AsciiString("foo"), new AsciiString("goo"))
                    .set(new AsciiString("foo2"), new AsciiString("goo2"))
                    .add(new AsciiString("foo2"), new AsciiString("goo3"));
            runInChannel(clientChannel, new Http2Runnable() {
                @Override
                public void run() {
                    clientHandler.encoder().writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient());
                    clientHandler.encoder().writeHeaders(ctxClient(), 3, http2Headers2, 0, false, newPromiseClient());
                    clientHandler.encoder().writeData(ctxClient(), 3, content.retain(), 0, true, newPromiseClient());
                    clientChannel.flush();
                }
            });
            awaitRequests();
            ArgumentCaptor<FullHttpMessage> requestCaptor = ArgumentCaptor.forClass(FullHttpMessage.class);
            verify(serverListener).messageReceived(requestCaptor.capture());
            capturedRequests = requestCaptor.getAllValues();
            assertEquals(request, capturedRequests.get(0));
        } finally {
            request.release();
        }
    }

    @Test
    public void clientRequestTrailingHeaders() throws Exception {
        boostrapEnv(1, 1, 1);
        final String text = "some data";
        final ByteBuf content = Unpooled.copiedBuffer(text.getBytes());
        final FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET,
                "/some/path/resource2", content, true);
        try {
            HttpHeaders httpHeaders = request.headers();
            httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
            httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length());
            httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16);
            HttpHeaders trailingHeaders = request.trailingHeaders();
            trailingHeaders.set(of("Foo"), of("goo"));
            trailingHeaders.set(of("fOo2"), of("goo2"));
            trailingHeaders.add(of("foO2"), of("goo3"));
            final Http2Headers http2Headers = new DefaultHttp2Headers().method(new AsciiString("GET")).path(
                    new AsciiString("/some/path/resource2"));
            final Http2Headers http2Headers2 = new DefaultHttp2Headers()
                    .set(new AsciiString("foo"), new AsciiString("goo"))
                    .set(new AsciiString("foo2"), new AsciiString("goo2"))
                    .add(new AsciiString("foo2"), new AsciiString("goo3"));
            runInChannel(clientChannel, new Http2Runnable() {
                @Override
                public void run() {
                    clientHandler.encoder().writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient());
                    clientHandler.encoder().writeData(ctxClient(), 3, content.retainedDuplicate(), 0, false,
                                                      newPromiseClient());
                    clientHandler.encoder().writeHeaders(ctxClient(), 3, http2Headers2, 0, true, newPromiseClient());
                    clientChannel.flush();
                }
            });
            awaitRequests();
            ArgumentCaptor<FullHttpMessage> requestCaptor = ArgumentCaptor.forClass(FullHttpMessage.class);
            verify(serverListener).messageReceived(requestCaptor.capture());
            capturedRequests = requestCaptor.getAllValues();
            assertEquals(request, capturedRequests.get(0));
        } finally {
            request.release();
        }
    }

    @Test
    public void clientRequestStreamDependencyInHttpMessageFlow() throws Exception {
        boostrapEnv(1, 2, 1);
        final String text = "hello world big time data!";
        final ByteBuf content = Unpooled.copiedBuffer(text.getBytes());
        final String text2 = "hello world big time data...number 2!!";
        final ByteBuf content2 = Unpooled.copiedBuffer(text2.getBytes());
        final FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.PUT,
                "/some/path/resource", content, true);
        final FullHttpMessage request2 = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.PUT,
                "/some/path/resource2", content2, true);
        try {
            HttpHeaders httpHeaders = request.headers();
            httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
            httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length());
            httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16);
            HttpHeaders httpHeaders2 = request2.headers();
            httpHeaders2.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 5);
            httpHeaders2.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_DEPENDENCY_ID.text(), 3);
            httpHeaders2.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 123);
            httpHeaders2.setInt(HttpHeaderNames.CONTENT_LENGTH, text2.length());
            final Http2Headers http2Headers = new DefaultHttp2Headers().method(new AsciiString("PUT")).path(
                    new AsciiString("/some/path/resource"));
            final Http2Headers http2Headers2 = new DefaultHttp2Headers().method(new AsciiString("PUT")).path(
                    new AsciiString("/some/path/resource2"));
            runInChannel(clientChannel, new Http2Runnable() {
                @Override
                public void run() {
                    clientHandler.encoder().writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient());
                    clientHandler.encoder().writeHeaders(ctxClient(), 5, http2Headers2, 0, false, newPromiseClient());
                    clientChannel.flush(); // Headers are queued in the flow controller and so flush them.
                    clientHandler.encoder().writePriority(ctxClient(), 5, 3, (short) 123, true, newPromiseClient());
                    clientHandler.encoder().writeData(ctxClient(), 3, content.retainedDuplicate(), 0, true,
                                                      newPromiseClient());
                    clientHandler.encoder().writeData(ctxClient(), 5, content2.retainedDuplicate(), 0, true,
                                                      newPromiseClient());
                    clientChannel.flush();
                }
            });
            awaitRequests();
            ArgumentCaptor<FullHttpMessage> httpObjectCaptor = ArgumentCaptor.forClass(FullHttpMessage.class);
            verify(serverListener, times(2)).messageReceived(httpObjectCaptor.capture());
            capturedRequests = httpObjectCaptor.getAllValues();
            assertEquals(request, capturedRequests.get(0));
            assertEquals(request2, capturedRequests.get(1));
        } finally {
            request.release();
            request2.release();
        }
    }

    @Test
    public void clientRequestStreamDependencyOutsideHttpMessageFlow() throws Exception {
        boostrapEnv(1, 3, 1);
        final String text = "hello world big time data!";
        final ByteBuf content = Unpooled.copiedBuffer(text.getBytes());
        final String text2 = "hello world big time data...number 2!!";
        final ByteBuf content2 = Unpooled.copiedBuffer(text2.getBytes());
        final FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.PUT,
                "/some/path/resource", content, true);
        final FullHttpMessage request2 = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.PUT,
                "/some/path/resource2", content2, true);
        final FullHttpMessage request3 = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1,
                HttpConversionUtil.OUT_OF_MESSAGE_SEQUENCE_METHOD,
                HttpConversionUtil.OUT_OF_MESSAGE_SEQUENCE_PATH, true);
        try {
            HttpHeaders httpHeaders = request.headers();
            httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
            httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length());
            httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16);
            HttpHeaders httpHeaders2 = request2.headers();
            httpHeaders2.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 5);
            httpHeaders2.setInt(HttpHeaderNames.CONTENT_LENGTH, text2.length());
            httpHeaders2.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16);
            final Http2Headers http2Headers = new DefaultHttp2Headers().method(new AsciiString("PUT")).path(
                    new AsciiString("/some/path/resource"));
            final Http2Headers http2Headers2 = new DefaultHttp2Headers().method(new AsciiString("PUT")).path(
                    new AsciiString("/some/path/resource2"));
            HttpHeaders httpHeaders3 = request3.headers();
            httpHeaders3.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 5);
            httpHeaders3.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_DEPENDENCY_ID.text(), 3);
            httpHeaders3.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 222);
            httpHeaders3.setInt(HttpHeaderNames.CONTENT_LENGTH, 0);
            runInChannel(clientChannel, new Http2Runnable() {
                @Override
                public void run() {
                    clientHandler.encoder().writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient());
                    clientHandler.encoder().writeHeaders(ctxClient(), 5, http2Headers2, 0, false, newPromiseClient());
                    clientHandler.encoder().writeData(ctxClient(), 3, content.retainedDuplicate(), 0, true,
                                                      newPromiseClient());
                    clientHandler.encoder().writeData(ctxClient(), 5, content2.retainedDuplicate(), 0, true,
                                                      newPromiseClient());
                    clientChannel.flush(); // headers and data are queued in the flow controller, so flush them.
                    clientHandler.encoder().writePriority(ctxClient(), 5, 3, (short) 222, false, newPromiseClient());
                    clientChannel.flush();
                }
            });
            awaitRequests();
            ArgumentCaptor<FullHttpMessage> httpObjectCaptor = ArgumentCaptor.forClass(FullHttpMessage.class);
            verify(serverListener, times(3)).messageReceived(httpObjectCaptor.capture());
            capturedRequests = httpObjectCaptor.getAllValues();
            assertEquals(request, capturedRequests.get(0));
            assertEquals(request2, capturedRequests.get(1));
            assertEquals(request3, capturedRequests.get(2));
        } finally {
            request.release();
            request2.release();
            request3.release();
        }
    }

    @Test
    public void serverRequestPushPromise() throws Exception {
        boostrapEnv(1, 1, 1);
        final String text = "hello world big time data!";
        final ByteBuf content = Unpooled.copiedBuffer(text.getBytes());
        final String text2 = "hello world smaller data?";
        final ByteBuf content2 = Unpooled.copiedBuffer(text2.getBytes());
        final FullHttpMessage response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK,
                content, true);
        final FullHttpMessage response2 = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CREATED,
                content2, true);
        final FullHttpMessage request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/push/test",
                true);
        try {
            HttpHeaders httpHeaders = response.headers();
            httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
            httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length());
            httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16);
            HttpHeaders httpHeaders2 = response2.headers();
            httpHeaders2.set(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), "https");
            httpHeaders2.set(HttpHeaderNames.HOST, "example.org");
            httpHeaders2.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 5);
            httpHeaders2.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_PROMISE_ID.text(), 3);
            httpHeaders2.setInt(HttpHeaderNames.CONTENT_LENGTH, text2.length());

            httpHeaders = request.headers();
            httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
            httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, 0);
            httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16);
            final Http2Headers http2Headers3 = new DefaultHttp2Headers().method(new AsciiString("GET"))
                    .path(new AsciiString("/push/test"));
            runInChannel(clientChannel, new Http2Runnable() {
                @Override
                public void run() {
                    clientHandler.encoder().writeHeaders(ctxClient(), 3, http2Headers3, 0, true, newPromiseClient());
                    clientChannel.flush();
                }
            });
            awaitRequests();
            ArgumentCaptor<FullHttpMessage> requestCaptor = ArgumentCaptor.forClass(FullHttpMessage.class);
            verify(serverListener).messageReceived(requestCaptor.capture());
            capturedRequests = requestCaptor.getAllValues();
            assertEquals(request, capturedRequests.get(0));

            final Http2Headers http2Headers = new DefaultHttp2Headers().status(new AsciiString("200"));
            final Http2Headers http2Headers2 = new DefaultHttp2Headers().status(new AsciiString("201"))
                    .scheme(new AsciiString("https"))
                    .authority(new AsciiString("example.org"));
            runInChannel(serverConnectedChannel, new Http2Runnable() {
                @Override
                public void run() {
                    serverHandler.encoder().writeHeaders(ctxServer(), 3, http2Headers, 0, false, newPromiseServer());
                    serverHandler.encoder().writePushPromise(ctxServer(), 3, 2, http2Headers2, 0, newPromiseServer());
                    serverHandler.encoder().writeData(ctxServer(), 3, content.retainedDuplicate(), 0, true,
                                                      newPromiseServer());
                    serverHandler.encoder().writeData(ctxServer(), 5, content2.retainedDuplicate(), 0, true,
                                                      newPromiseServer());
                    serverConnectedChannel.flush();
                }
            });
            awaitResponses();
            ArgumentCaptor<FullHttpMessage> responseCaptor = ArgumentCaptor.forClass(FullHttpMessage.class);
            verify(clientListener).messageReceived(responseCaptor.capture());
            capturedResponses = responseCaptor.getAllValues();
            assertEquals(response, capturedResponses.get(0));
        } finally {
            request.release();
            response.release();
            response2.release();
        }
    }

    @Test
    public void serverResponseHeaderInformational() throws Exception {
        boostrapEnv(1, 2, 1, 2, 1);
        final FullHttpMessage request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.PUT, "/info/test",
                true);
        HttpHeaders httpHeaders = request.headers();
        httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
        httpHeaders.set(HttpHeaderNames.EXPECT, HttpHeaderValues.CONTINUE);
        httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, 0);
        httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16);

        final Http2Headers http2Headers = new DefaultHttp2Headers().method(new AsciiString("PUT"))
                .path(new AsciiString("/info/test"))
                .set(new AsciiString(HttpHeaderNames.EXPECT.toString()),
                     new AsciiString(HttpHeaderValues.CONTINUE.toString()));
        final FullHttpMessage response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
        final String text = "a big payload";
        final ByteBuf payload = Unpooled.copiedBuffer(text.getBytes());
        final FullHttpMessage request2 = request.replace(payload);
        final FullHttpMessage response2 = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);

        try {
            runInChannel(clientChannel, new Http2Runnable() {
                @Override
                public void run() {
                    clientHandler.encoder().writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient());
                    clientChannel.flush();
                }
            });

            awaitRequests();
            httpHeaders = response.headers();
            httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
            httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, 0);
            final Http2Headers http2HeadersResponse = new DefaultHttp2Headers().status(new AsciiString("100"));
            runInChannel(serverConnectedChannel, new Http2Runnable() {
                @Override
                public void run() {
                    serverHandler.encoder().writeHeaders(ctxServer(), 3, http2HeadersResponse, 0, false,
                                                         newPromiseServer());
                    serverConnectedChannel.flush();
                }
            });

            awaitResponses();
            httpHeaders = request2.headers();
            httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length());
            httpHeaders.remove(HttpHeaderNames.EXPECT);
            runInChannel(clientChannel, new Http2Runnable() {
                @Override
                public void run() {
                    clientHandler.encoder().writeData(ctxClient(), 3, payload.retainedDuplicate(), 0, true,
                                                      newPromiseClient());
                    clientChannel.flush();
                }
            });

            awaitRequests2();
            httpHeaders = response2.headers();
            httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
            httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, 0);
            httpHeaders.setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), (short) 16);

            final Http2Headers http2HeadersResponse2 = new DefaultHttp2Headers().status(new AsciiString("200"));
            runInChannel(serverConnectedChannel, new Http2Runnable() {
                @Override
                public void run() {
                    serverHandler.encoder().writeHeaders(ctxServer(), 3, http2HeadersResponse2, 0, true,
                                                         newPromiseServer());
                    serverConnectedChannel.flush();
                }
            });

            awaitResponses2();
            ArgumentCaptor<FullHttpMessage> requestCaptor = ArgumentCaptor.forClass(FullHttpMessage.class);
            verify(serverListener, times(2)).messageReceived(requestCaptor.capture());
            capturedRequests = requestCaptor.getAllValues();
            assertEquals(2, capturedRequests.size());
            assertEquals(request, capturedRequests.get(0));
            assertEquals(request2, capturedRequests.get(1));

            ArgumentCaptor<FullHttpMessage> responseCaptor = ArgumentCaptor.forClass(FullHttpMessage.class);
            verify(clientListener, times(2)).messageReceived(responseCaptor.capture());
            capturedResponses = responseCaptor.getAllValues();
            assertEquals(2, capturedResponses.size());
            assertEquals(response, capturedResponses.get(0));
            assertEquals(response2, capturedResponses.get(1));
        } finally {
            request.release();
            request2.release();
            response.release();
            response2.release();
        }
    }

    @Test
    public void propagateSettings() throws Exception {
        boostrapEnv(1, 1, 2);
        final Http2Settings settings = new Http2Settings().pushEnabled(true);
        runInChannel(clientChannel, new Http2Runnable() {
            @Override
            public void run() {
                clientHandler.encoder().writeSettings(ctxClient(), settings, newPromiseClient());
                clientChannel.flush();
            }
        });
        assertTrue(settingsLatch.await(3, SECONDS));
        ArgumentCaptor<Http2Settings> settingsCaptor = ArgumentCaptor.forClass(Http2Settings.class);
        verify(settingsListener, times(2)).messageReceived(settingsCaptor.capture());
        assertEquals(settings, settingsCaptor.getValue());
    }

    private void boostrapEnv(int clientLatchCount, int serverLatchCount, int settingsLatchCount)
                throws InterruptedException {
        boostrapEnv(clientLatchCount, clientLatchCount, serverLatchCount, serverLatchCount, settingsLatchCount);
    }

    private void boostrapEnv(int clientLatchCount, int clientLatchCount2, int serverLatchCount, int serverLatchCount2,
            int settingsLatchCount) throws InterruptedException {
        clientDelegator = null;
        serverDelegator = null;
        serverConnectedChannel = null;
        maxContentLength = 1024;
        final CountDownLatch serverChannelLatch = new CountDownLatch(1);
        serverLatch = new CountDownLatch(serverLatchCount);
        clientLatch = new CountDownLatch(clientLatchCount);
        serverLatch2 = new CountDownLatch(serverLatchCount2);
        clientLatch2 = new CountDownLatch(clientLatchCount2);
        settingsLatch = new CountDownLatch(settingsLatchCount);

        sb = new ServerBootstrap();
        cb = new Bootstrap();

        sb.group(new DefaultEventLoopGroup());
        sb.channel(LocalServerChannel.class);
        sb.childHandler(new ChannelInitializer<Channel>() {
            @Override
            protected void initChannel(Channel ch) throws Exception {
                serverConnectedChannel = ch;
                ChannelPipeline p = ch.pipeline();
                Http2Connection connection = new DefaultHttp2Connection(true);

                serverHandler = new Http2ConnectionHandlerBuilder().frameListener(
                        new InboundHttp2ToHttpPriorityAdapterBuilder(connection)
                           .maxContentLength(maxContentLength)
                           .validateHttpHeaders(true)
                           .propagateSettings(true)
                           .build())
                   .connection(connection)
                   .gracefulShutdownTimeoutMillis(0)
                   .build();
                p.addLast(serverHandler);

                serverDelegator = new HttpResponseDelegator(serverListener, serverLatch, serverLatch2);
                p.addLast(serverDelegator);
                settingsDelegator = new HttpSettingsDelegator(settingsListener, settingsLatch);
                p.addLast(settingsDelegator);
                serverChannelLatch.countDown();
            }
        });

        cb.group(new DefaultEventLoopGroup());
        cb.channel(LocalChannel.class);
        cb.handler(new ChannelInitializer<Channel>() {
            @Override
            protected void initChannel(Channel ch) throws Exception {
                ChannelPipeline p = ch.pipeline();
                Http2Connection connection = new DefaultHttp2Connection(false);

                clientHandler = new Http2ConnectionHandlerBuilder().frameListener(
                        new InboundHttp2ToHttpPriorityAdapterBuilder(connection)
                           .maxContentLength(maxContentLength)
                           .build())
                   .connection(connection)
                   .gracefulShutdownTimeoutMillis(0)
                   .build();
                p.addLast(clientHandler);

                clientDelegator = new HttpResponseDelegator(clientListener, clientLatch, clientLatch2);
                p.addLast(clientDelegator);
                p.addLast(new ChannelHandlerAdapter() {
                    @Override
                    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
                        Http2Exception e = getEmbeddedHttp2Exception(cause);
                        if (e != null) {
                            clientException = e;
                            clientLatch.countDown();
                        } else {
                            super.exceptionCaught(ctx, cause);
                        }
                    }
                });
            }
        });

        serverChannel = sb.bind(new LocalAddress("InboundHttp2ToHttpAdapterTest")).sync().channel();

        ChannelFuture ccf = cb.connect(serverChannel.localAddress());
        assertTrue(ccf.awaitUninterruptibly().isSuccess());
        clientChannel = ccf.channel();
        assertTrue(serverChannelLatch.await(2, SECONDS));
    }

    private void cleanupCapturedRequests() {
        if (capturedRequests != null) {
            for (FullHttpMessage capturedRequest : capturedRequests) {
                capturedRequest.release();
            }
            capturedRequests = null;
        }
    }

    private void cleanupCapturedResponses() {
        if (capturedResponses != null) {
            for (FullHttpMessage capturedResponse : capturedResponses) {
                capturedResponse.release();
            }
            capturedResponses = null;
        }
    }

    private void awaitRequests() throws Exception {
        assertTrue(serverLatch.await(3, SECONDS));
    }

    private void awaitResponses() throws Exception {
        assertTrue(clientLatch.await(3, SECONDS));
    }

    private void awaitRequests2() throws Exception {
        assertTrue(serverLatch2.await(3, SECONDS));
    }

    private void awaitResponses2() throws Exception {
        assertTrue(clientLatch2.await(3, SECONDS));
    }

    private ChannelHandlerContext ctxClient() {
        return clientChannel.pipeline().firstContext();
    }

    private ChannelPromise newPromiseClient() {
        return ctxClient().newPromise();
    }

    private ChannelHandlerContext ctxServer() {
        return serverConnectedChannel.pipeline().firstContext();
    }

    private ChannelPromise newPromiseServer() {
        return ctxServer().newPromise();
    }

    private interface HttpResponseListener {
        void messageReceived(HttpObject obj);
    }

    private interface HttpSettingsListener {
        void messageReceived(Http2Settings settings);
    }

    private static final class HttpResponseDelegator extends SimpleChannelInboundHandler<HttpObject> {
        private final HttpResponseListener listener;
        private final CountDownLatch latch;
        private final CountDownLatch latch2;

        HttpResponseDelegator(HttpResponseListener listener, CountDownLatch latch, CountDownLatch latch2) {
            super(false);
            this.listener = listener;
            this.latch = latch;
            this.latch2 = latch2;
        }

        @Override
        protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
            listener.messageReceived(msg);
            latch.countDown();
            latch2.countDown();
        }
    }

    private static final class HttpSettingsDelegator extends SimpleChannelInboundHandler<Http2Settings> {
        private final HttpSettingsListener listener;
        private final CountDownLatch latch;

        HttpSettingsDelegator(HttpSettingsListener listener, CountDownLatch latch) {
            super(false);
            this.listener = listener;
            this.latch = latch;
        }

        @Override
        protected void channelRead0(ChannelHandlerContext ctx, Http2Settings settings) throws Exception {
            listener.messageReceived(settings);
            latch.countDown();
        }
    }
}
