/*
 * 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:
 *
 * https://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.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.ChannelPromise;
import io.netty.channel.DefaultEventLoopGroup;
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.DefaultHttpContent;
import io.netty.handler.codec.http.DefaultHttpRequest;
import io.netty.handler.codec.http.DefaultLastHttpContent;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpScheme;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.codec.http2.Http2TestUtil.FrameCountDown;
import io.netty.util.AsciiString;
import io.netty.util.concurrent.Future;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

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

import static io.netty.handler.codec.http.HttpMethod.CONNECT;
import static io.netty.handler.codec.http.HttpMethod.GET;
import static io.netty.handler.codec.http.HttpMethod.OPTIONS;
import static io.netty.handler.codec.http.HttpMethod.POST;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
import static io.netty.handler.codec.http2.Http2TestUtil.of;
import static io.netty.util.CharsetUtil.UTF_8;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyBoolean;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.anyShort;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;

/**
 * Testing the {@link HttpToHttp2ConnectionHandler} for {@link FullHttpRequest} objects into HTTP/2 frames
 */
public class HttpToHttp2ConnectionHandlerTest {
    private static final int WAIT_TIME_SECONDS = 5;

    @Mock
    private Http2FrameListener clientListener;

    @Mock
    private Http2FrameListener serverListener;

    private ServerBootstrap sb;
    private Bootstrap cb;
    private Channel serverChannel;
    private volatile Channel serverConnectedChannel;
    private Channel clientChannel;
    private CountDownLatch requestLatch;
    private CountDownLatch serverSettingsAckLatch;
    private CountDownLatch trailersLatch;
    private FrameCountDown serverFrameCountDown;

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

    @AfterEach
    public void teardown() throws Exception {
        if (clientChannel != null) {
            clientChannel.close().syncUninterruptibly();
            clientChannel = null;
        }
        if (serverChannel != null) {
            serverChannel.close().syncUninterruptibly();
            serverChannel = null;
        }
        final Channel serverConnectedChannel = this.serverConnectedChannel;
        if (serverConnectedChannel != null) {
            serverConnectedChannel.close().syncUninterruptibly();
            this.serverConnectedChannel = null;
        }
        Future<?> serverGroup = sb.config().group().shutdownGracefully(0, 5, SECONDS);
        Future<?> serverChildGroup = sb.config().childGroup().shutdownGracefully(0, 5, SECONDS);
        Future<?> clientGroup = cb.config().group().shutdownGracefully(0, 5, SECONDS);
        serverGroup.syncUninterruptibly();
        serverChildGroup.syncUninterruptibly();
        clientGroup.syncUninterruptibly();
    }

    @Test
    public void testHeadersOnlyRequest() throws Exception {
        bootstrapEnv(2, 1, 0);
        final FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, GET,
                "http://my-user_name@www.example.org:5555/example");
        final HttpHeaders httpHeaders = request.headers();
        httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 5);
        httpHeaders.set(HttpHeaderNames.HOST, "my-user_name@www.example.org:5555");
        httpHeaders.set(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), "http");
        httpHeaders.add(of("foo"), of("goo"));
        httpHeaders.add(of("foo"), of("goo2"));
        httpHeaders.add(of("foo2"), of("goo2"));
        final Http2Headers http2Headers =
                new DefaultHttp2Headers().method(new AsciiString("GET")).path(new AsciiString("/example"))
                .authority(new AsciiString("www.example.org:5555")).scheme(new AsciiString("http"))
                .add(new AsciiString("foo"), new AsciiString("goo"))
                .add(new AsciiString("foo"), new AsciiString("goo2"))
                .add(new AsciiString("foo2"), new AsciiString("goo2"));

        ChannelPromise writePromise = newPromise();
        verifyHeadersOnly(http2Headers, writePromise, clientChannel.writeAndFlush(request, writePromise));
    }

    @Test
    public void testHttpScheme() throws Exception {
        bootstrapEnv(2, 1, 0);
        final FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, GET,
                "http://my-user_name@www.example.org:5555/example");
        final HttpHeaders httpHeaders = request.headers();
        httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 5);
        httpHeaders.set(HttpHeaderNames.HOST, "my-user_name@www.example.org:5555");
        httpHeaders.add(of("foo"), of("goo"));
        httpHeaders.add(of("foo"), of("goo2"));
        httpHeaders.add(of("foo2"), of("goo2"));
        final Http2Headers http2Headers =
                new DefaultHttp2Headers().method(new AsciiString("GET")).path(new AsciiString("/example"))
                        .authority(new AsciiString("www.example.org:5555")).scheme(new AsciiString("http"))
                        .scheme(new AsciiString("http"))
                        .add(new AsciiString("foo"), new AsciiString("goo"))
                        .add(new AsciiString("foo"), new AsciiString("goo2"))
                        .add(new AsciiString("foo2"), new AsciiString("goo2"));

        ChannelPromise writePromise = newPromise();
        verifyHeadersOnly(http2Headers, writePromise, clientChannel.writeAndFlush(request, writePromise));
    }

    @Test
    public void testMultipleCookieEntriesAreCombined() throws Exception {
        bootstrapEnv(2, 1, 0);
        final FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, GET,
                "http://my-user_name@www.example.org:5555/example");
        final HttpHeaders httpHeaders = request.headers();
        httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 5);
        httpHeaders.set(HttpHeaderNames.HOST, "my-user_name@www.example.org:5555");
        httpHeaders.set(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), "http");
        httpHeaders.set(HttpHeaderNames.COOKIE, "a=b; c=d; e=f");
        final Http2Headers http2Headers =
                new DefaultHttp2Headers().method(new AsciiString("GET")).path(new AsciiString("/example"))
                .authority(new AsciiString("www.example.org:5555")).scheme(new AsciiString("http"))
                .add(HttpHeaderNames.COOKIE, "a=b")
                .add(HttpHeaderNames.COOKIE, "c=d")
                .add(HttpHeaderNames.COOKIE, "e=f");

        ChannelPromise writePromise = newPromise();
        verifyHeadersOnly(http2Headers, writePromise, clientChannel.writeAndFlush(request, writePromise));
    }

    @Test
    public void testOriginFormRequestTargetHandled() throws Exception {
        bootstrapEnv(2, 1, 0);
        final FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, GET, "/where?q=now&f=then#section1");
        final HttpHeaders httpHeaders = request.headers();
        httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 5);
        httpHeaders.set(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), "http");
        final Http2Headers http2Headers =
                new DefaultHttp2Headers().method(new AsciiString("GET"))
                .path(new AsciiString("/where?q=now&f=then#section1"))
                .scheme(new AsciiString("http"));

        ChannelPromise writePromise = newPromise();
        verifyHeadersOnly(http2Headers, writePromise, clientChannel.writeAndFlush(request, writePromise));
    }

    @Test
    public void testOriginFormRequestTargetHandledFromUrlencodedUri() throws Exception {
        bootstrapEnv(2, 1, 0);
        final FullHttpRequest request = new DefaultFullHttpRequest(
                HTTP_1_1, GET, "/where%2B0?q=now%2B0&f=then%2B0#section1%2B0");
        final HttpHeaders httpHeaders = request.headers();
        httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 5);
        httpHeaders.set(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), "http");
        final Http2Headers http2Headers =
                new DefaultHttp2Headers().method(new AsciiString("GET"))
                                         .path(new AsciiString("/where%2B0?q=now%2B0&f=then%2B0#section1%2B0"))
                                         .scheme(new AsciiString("http"));

        ChannelPromise writePromise = newPromise();
        verifyHeadersOnly(http2Headers, writePromise, clientChannel.writeAndFlush(request, writePromise));
    }

    @Test
    public void testAbsoluteFormRequestTargetHandledFromHeaders() throws Exception {
        bootstrapEnv(2, 1, 0);
        final FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, GET, "/pub/WWW/TheProject.html");
        final HttpHeaders httpHeaders = request.headers();
        httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 5);
        httpHeaders.set(HttpHeaderNames.HOST, "foouser@www.example.org:5555");
        httpHeaders.set(HttpConversionUtil.ExtensionHeaderNames.PATH.text(), "ignored_path");
        httpHeaders.set(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), "https");
        final Http2Headers http2Headers =
                new DefaultHttp2Headers().method(new AsciiString("GET"))
                .path(new AsciiString("/pub/WWW/TheProject.html"))
                .authority(new AsciiString("www.example.org:5555")).scheme(new AsciiString("https"));

        ChannelPromise writePromise = newPromise();
        verifyHeadersOnly(http2Headers, writePromise, clientChannel.writeAndFlush(request, writePromise));
    }

    @Test
    public void testAbsoluteFormRequestTargetHandledFromRequestTargetUri() throws Exception {
        bootstrapEnv(2, 1, 0);
        final FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, GET,
                "http://foouser@www.example.org:5555/pub/WWW/TheProject.html");
        final HttpHeaders httpHeaders = request.headers();
        httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 5);
        final Http2Headers http2Headers =
                new DefaultHttp2Headers().method(new AsciiString("GET"))
                .path(new AsciiString("/pub/WWW/TheProject.html"))
                .authority(new AsciiString("www.example.org:5555")).scheme(new AsciiString("http"));

        ChannelPromise writePromise = newPromise();
        verifyHeadersOnly(http2Headers, writePromise, clientChannel.writeAndFlush(request, writePromise));
    }

    @Test
    public void testAuthorityFormRequestTargetHandled() throws Exception {
        bootstrapEnv(2, 1, 0);
        final FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, CONNECT, "http://www.example.com:80");
        final HttpHeaders httpHeaders = request.headers();
        httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 5);
        final Http2Headers http2Headers =
                new DefaultHttp2Headers().method(new AsciiString("CONNECT")).path(new AsciiString("/"))
                .scheme(new AsciiString("http")).authority(new AsciiString("www.example.com:80"));

        ChannelPromise writePromise = newPromise();
        verifyHeadersOnly(http2Headers, writePromise, clientChannel.writeAndFlush(request, writePromise));
    }

    @Test
    public void testAsterikFormRequestTargetHandled() throws Exception {
        bootstrapEnv(2, 1, 0);
        final FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, OPTIONS, "*");
        final HttpHeaders httpHeaders = request.headers();
        httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 5);
        httpHeaders.set(HttpHeaderNames.HOST, "www.example.com:80");
        httpHeaders.set(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), "http");
        final Http2Headers http2Headers =
                new DefaultHttp2Headers().method(new AsciiString("OPTIONS")).path(new AsciiString("*"))
                .scheme(new AsciiString("http")).authority(new AsciiString("www.example.com:80"));

        ChannelPromise writePromise = newPromise();
        verifyHeadersOnly(http2Headers, writePromise, clientChannel.writeAndFlush(request, writePromise));
    }

    @Test
    public void testHostIPv6FormRequestTargetHandled() throws Exception {
        // Valid according to
        // https://tools.ietf.org/html/rfc7230#section-2.7.1 -> https://tools.ietf.org/html/rfc3986#section-3.2.2
        bootstrapEnv(2, 1, 0);
        final FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, GET, "/");
        final HttpHeaders httpHeaders = request.headers();
        httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 5);
        httpHeaders.set(HttpHeaderNames.HOST, "[::1]:80");
        httpHeaders.set(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), "http");
        final Http2Headers http2Headers =
                new DefaultHttp2Headers().method(new AsciiString("GET")).path(new AsciiString("/"))
                .scheme(new AsciiString("http")).authority(new AsciiString("[::1]:80"));

        ChannelPromise writePromise = newPromise();
        verifyHeadersOnly(http2Headers, writePromise, clientChannel.writeAndFlush(request, writePromise));
    }

    @Test
    public void testHostFormRequestTargetHandled() throws Exception {
        bootstrapEnv(2, 1, 0);
        final FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, GET, "/");
        final HttpHeaders httpHeaders = request.headers();
        httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 5);
        httpHeaders.set(HttpHeaderNames.HOST, "localhost:80");
        httpHeaders.set(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), "http");
        final Http2Headers http2Headers =
                new DefaultHttp2Headers().method(new AsciiString("GET")).path(new AsciiString("/"))
                .scheme(new AsciiString("http")).authority(new AsciiString("localhost:80"));

        ChannelPromise writePromise = newPromise();
        verifyHeadersOnly(http2Headers, writePromise, clientChannel.writeAndFlush(request, writePromise));
    }

    @Test
    public void testHostIPv4FormRequestTargetHandled() throws Exception {
        bootstrapEnv(2, 1, 0);
        final FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, GET, "/");
        final HttpHeaders httpHeaders = request.headers();
        httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 5);
        httpHeaders.set(HttpHeaderNames.HOST, "1.2.3.4:80");
        httpHeaders.set(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), "http");
        final Http2Headers http2Headers =
                new DefaultHttp2Headers().method(new AsciiString("GET")).path(new AsciiString("/"))
                .scheme(new AsciiString("http")).authority(new AsciiString("1.2.3.4:80"));

        ChannelPromise writePromise = newPromise();
        verifyHeadersOnly(http2Headers, writePromise, clientChannel.writeAndFlush(request, writePromise));
    }

    @Test
    public void testNoSchemeRequestTargetHandled() throws Exception {
        bootstrapEnv(2, 1, 0);
        final FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, GET, "/");
        final HttpHeaders httpHeaders = request.headers();
        httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), 5);
        httpHeaders.set(HttpHeaderNames.HOST, "localhost");
        ChannelPromise writePromise = newPromise();
        ChannelFuture writeFuture = clientChannel.writeAndFlush(request, writePromise);

        assertTrue(writePromise.awaitUninterruptibly(WAIT_TIME_SECONDS, SECONDS));
        assertTrue(writePromise.isDone());
        assertFalse(writePromise.isSuccess());
        assertTrue(writeFuture.isDone());
        assertFalse(writeFuture.isSuccess());
    }

    @Test
    public void testInvalidStreamId() throws Exception {
        bootstrapEnv(2, 1, 0);
        final FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, POST, "/foo",
                Unpooled.copiedBuffer("foobar", UTF_8));
        final HttpHeaders httpHeaders = request.headers();
        httpHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), -1);
        httpHeaders.set(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), "http");
        httpHeaders.set(HttpHeaderNames.HOST, "localhost");
        ChannelPromise writePromise = newPromise();
        ChannelFuture writeFuture = clientChannel.writeAndFlush(request, writePromise);

        assertTrue(writePromise.awaitUninterruptibly(WAIT_TIME_SECONDS, SECONDS));
        assertTrue(writePromise.isDone());
        assertFalse(writePromise.isSuccess());
        Throwable cause = writePromise.cause();
        assertThat(cause, instanceOf(Http2NoMoreStreamIdsException.class));

        assertTrue(writeFuture.isDone());
        assertFalse(writeFuture.isSuccess());
        cause = writeFuture.cause();
        assertThat(cause, instanceOf(Http2NoMoreStreamIdsException.class));
    }

    @Test
    public void testRequestWithBody() throws Exception {
        final String text = "foooooogoooo";
        final List<String> receivedBuffers = Collections.synchronizedList(new ArrayList<String>());
        doAnswer(new Answer<Void>() {
            @Override
            public Void answer(InvocationOnMock in) throws Throwable {
                receivedBuffers.add(((ByteBuf) in.getArguments()[2]).toString(UTF_8));
                return null;
            }
        }).when(serverListener).onDataRead(any(ChannelHandlerContext.class), eq(3),
                any(ByteBuf.class), eq(0), eq(true));
        bootstrapEnv(3, 1, 0);
        final FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, POST,
                "http://your_user-name123@www.example.org:5555/example",
                Unpooled.copiedBuffer(text, UTF_8));
        final HttpHeaders httpHeaders = request.headers();
        httpHeaders.set(HttpHeaderNames.HOST, "www.example-origin.org:5555");
        httpHeaders.add(of("foo"), of("goo"));
        httpHeaders.add(of("foo"), of("goo2"));
        httpHeaders.add(of("foo2"), of("goo2"));
        final Http2Headers http2Headers =
                new DefaultHttp2Headers().method(new AsciiString("POST")).path(new AsciiString("/example"))
                .authority(new AsciiString("www.example-origin.org:5555")).scheme(new AsciiString("http"))
                .add(new AsciiString("foo"), new AsciiString("goo"))
                .add(new AsciiString("foo"), new AsciiString("goo2"))
                .add(new AsciiString("foo2"), new AsciiString("goo2"));
        ChannelPromise writePromise = newPromise();
        ChannelFuture writeFuture = clientChannel.writeAndFlush(request, writePromise);

        assertTrue(writePromise.awaitUninterruptibly(WAIT_TIME_SECONDS, SECONDS));
        assertTrue(writePromise.isSuccess());
        assertTrue(writeFuture.awaitUninterruptibly(WAIT_TIME_SECONDS, SECONDS));
        assertTrue(writeFuture.isSuccess());
        awaitRequests();
        verify(serverListener).onHeadersRead(any(ChannelHandlerContext.class), eq(3), eq(http2Headers), eq(0),
                anyShort(), anyBoolean(), eq(0), eq(false));
        verify(serverListener).onDataRead(any(ChannelHandlerContext.class), eq(3), any(ByteBuf.class), eq(0),
                eq(true));
        assertEquals(1, receivedBuffers.size());
        assertEquals(text, receivedBuffers.get(0));
    }

    @Test
    public void testRequestWithBodyAndTrailingHeaders() throws Exception {
        final String text = "foooooogoooo";
        final List<String> receivedBuffers = Collections.synchronizedList(new ArrayList<String>());
        doAnswer(new Answer<Void>() {
            @Override
            public Void answer(InvocationOnMock in) throws Throwable {
                receivedBuffers.add(((ByteBuf) in.getArguments()[2]).toString(UTF_8));
                return null;
            }
        }).when(serverListener).onDataRead(any(ChannelHandlerContext.class), eq(3),
                any(ByteBuf.class), eq(0), eq(false));
        bootstrapEnv(4, 1, 1);
        final FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, POST,
                "http://your_user-name123@www.example.org:5555/example",
                Unpooled.copiedBuffer(text, UTF_8));
        final HttpHeaders httpHeaders = request.headers();
        httpHeaders.set(HttpHeaderNames.HOST, "www.example.org:5555");
        httpHeaders.add(of("foo"), of("goo"));
        httpHeaders.add(of("foo"), of("goo2"));
        httpHeaders.add(of("foo2"), of("goo2"));
        final Http2Headers http2Headers =
                new DefaultHttp2Headers().method(new AsciiString("POST")).path(new AsciiString("/example"))
                        .authority(new AsciiString("www.example.org:5555")).scheme(new AsciiString("http"))
                        .add(new AsciiString("foo"), new AsciiString("goo"))
                        .add(new AsciiString("foo"), new AsciiString("goo2"))
                        .add(new AsciiString("foo2"), new AsciiString("goo2"));

        request.trailingHeaders().add(of("trailing"), of("bar"));

        final Http2Headers http2TrailingHeaders = new DefaultHttp2Headers()
                .add(new AsciiString("trailing"), new AsciiString("bar"));

        ChannelPromise writePromise = newPromise();
        ChannelFuture writeFuture = clientChannel.writeAndFlush(request, writePromise);

        assertTrue(writePromise.awaitUninterruptibly(WAIT_TIME_SECONDS, SECONDS));
        assertTrue(writePromise.isSuccess());
        assertTrue(writeFuture.awaitUninterruptibly(WAIT_TIME_SECONDS, SECONDS));
        assertTrue(writeFuture.isSuccess());
        awaitRequests();
        verify(serverListener).onHeadersRead(any(ChannelHandlerContext.class), eq(3), eq(http2Headers), eq(0),
                anyShort(), anyBoolean(), eq(0), eq(false));
        verify(serverListener).onDataRead(any(ChannelHandlerContext.class), eq(3), any(ByteBuf.class), eq(0),
                eq(false));
        verify(serverListener).onHeadersRead(any(ChannelHandlerContext.class), eq(3), eq(http2TrailingHeaders), eq(0),
                anyShort(), anyBoolean(), eq(0), eq(true));
        assertEquals(1, receivedBuffers.size());
        assertEquals(text, receivedBuffers.get(0));
    }

    @Test
    public void testChunkedRequestWithBodyAndTrailingHeaders() throws Exception {
        final String text = "foooooo";
        final String text2 = "goooo";
        final List<String> receivedBuffers = Collections.synchronizedList(new ArrayList<String>());
        doAnswer(new Answer<Void>() {
            @Override
            public Void answer(InvocationOnMock in) throws Throwable {
                receivedBuffers.add(((ByteBuf) in.getArguments()[2]).toString(UTF_8));
                return null;
            }
        }).when(serverListener).onDataRead(any(ChannelHandlerContext.class), eq(3),
                any(ByteBuf.class), eq(0), eq(false));
        bootstrapEnv(4, 1, 1);
        final HttpRequest request = new DefaultHttpRequest(HTTP_1_1, POST,
                "http://your_user-name123@www.example.org:5555/example");
        final HttpHeaders httpHeaders = request.headers();
        httpHeaders.set(HttpHeaderNames.HOST, "www.example.org:5555");
        httpHeaders.add(HttpHeaderNames.TRANSFER_ENCODING, "chunked");
        httpHeaders.add(of("foo"), of("goo"));
        httpHeaders.add(of("foo"), of("goo2"));
        httpHeaders.add(of("foo2"), of("goo2"));
        final Http2Headers http2Headers =
                new DefaultHttp2Headers().method(new AsciiString("POST")).path(new AsciiString("/example"))
                        .authority(new AsciiString("www.example.org:5555")).scheme(new AsciiString("http"))
                        .add(new AsciiString("foo"), new AsciiString("goo"))
                        .add(new AsciiString("foo"), new AsciiString("goo2"))
                        .add(new AsciiString("foo2"), new AsciiString("goo2"));

        final DefaultHttpContent httpContent = new DefaultHttpContent(Unpooled.copiedBuffer(text, UTF_8));
        final LastHttpContent lastHttpContent = new DefaultLastHttpContent(Unpooled.copiedBuffer(text2, UTF_8));

        lastHttpContent.trailingHeaders().add(of("trailing"), of("bar"));

        final Http2Headers http2TrailingHeaders = new DefaultHttp2Headers()
                .add(new AsciiString("trailing"), new AsciiString("bar"));

        ChannelPromise writePromise = newPromise();
        ChannelFuture writeFuture = clientChannel.write(request, writePromise);
        ChannelPromise contentPromise = newPromise();
        ChannelFuture contentFuture = clientChannel.write(httpContent, contentPromise);
        ChannelPromise lastContentPromise = newPromise();
        ChannelFuture lastContentFuture = clientChannel.write(lastHttpContent, lastContentPromise);

        clientChannel.flush();

        assertTrue(writePromise.awaitUninterruptibly(WAIT_TIME_SECONDS, SECONDS));
        assertTrue(writePromise.isSuccess());
        assertTrue(writeFuture.awaitUninterruptibly(WAIT_TIME_SECONDS, SECONDS));
        assertTrue(writeFuture.isSuccess());

        assertTrue(contentPromise.awaitUninterruptibly(WAIT_TIME_SECONDS, SECONDS));
        assertTrue(contentPromise.isSuccess());
        assertTrue(contentFuture.awaitUninterruptibly(WAIT_TIME_SECONDS, SECONDS));
        assertTrue(contentFuture.isSuccess());

        assertTrue(lastContentPromise.awaitUninterruptibly(WAIT_TIME_SECONDS, SECONDS));
        assertTrue(lastContentPromise.isSuccess());
        assertTrue(lastContentFuture.awaitUninterruptibly(WAIT_TIME_SECONDS, SECONDS));
        assertTrue(lastContentFuture.isSuccess());

        awaitRequests();
        verify(serverListener).onHeadersRead(any(ChannelHandlerContext.class), eq(3), eq(http2Headers), eq(0),
                anyShort(), anyBoolean(), eq(0), eq(false));
        verify(serverListener).onDataRead(any(ChannelHandlerContext.class), eq(3), any(ByteBuf.class), eq(0),
                eq(false));
        verify(serverListener).onHeadersRead(any(ChannelHandlerContext.class), eq(3), eq(http2TrailingHeaders), eq(0),
                anyShort(), anyBoolean(), eq(0), eq(true));
        assertEquals(1, receivedBuffers.size());
        assertEquals(text + text2, receivedBuffers.get(0));
    }

    private void bootstrapEnv(int requestCountDown, int serverSettingsAckCount, int trailersCount) throws Exception {
        final CountDownLatch prefaceWrittenLatch = new CountDownLatch(1);
        final CountDownLatch serverChannelLatch = new CountDownLatch(1);
        requestLatch = new CountDownLatch(requestCountDown);
        serverSettingsAckLatch = new CountDownLatch(serverSettingsAckCount);
        trailersLatch = trailersCount == 0 ? null : new CountDownLatch(trailersCount);

        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();
                serverFrameCountDown =
                        new FrameCountDown(serverListener, serverSettingsAckLatch, requestLatch, null, trailersLatch);
                p.addLast(new HttpToHttp2ConnectionHandlerBuilder()
                           .server(true)
                           .frameListener(serverFrameCountDown)
                           .httpScheme(HttpScheme.HTTP)
                           .build());
                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();
                HttpToHttp2ConnectionHandler handler = new HttpToHttp2ConnectionHandlerBuilder()
                        .server(false)
                        .frameListener(clientListener)
                        .gracefulShutdownTimeoutMillis(0)
                        .build();
                p.addLast(handler);
                p.addLast(new ChannelInboundHandlerAdapter() {
                    @Override
                    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
                        if (evt == Http2ConnectionPrefaceAndSettingsFrameWrittenEvent.INSTANCE) {
                            prefaceWrittenLatch.countDown();
                            ctx.pipeline().remove(this);
                        }
                    }
                });
            }
        });

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

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

    private void verifyHeadersOnly(Http2Headers expected, ChannelPromise writePromise, ChannelFuture writeFuture)
            throws Exception {
        assertTrue(writePromise.awaitUninterruptibly(WAIT_TIME_SECONDS, SECONDS));
        assertTrue(writePromise.isSuccess());
        assertTrue(writeFuture.awaitUninterruptibly(WAIT_TIME_SECONDS, SECONDS));
        assertTrue(writeFuture.isSuccess());
        awaitRequests();
        verify(serverListener).onHeadersRead(any(ChannelHandlerContext.class), eq(5),
                eq(expected), eq(0), anyShort(), anyBoolean(), eq(0), eq(true));
        verify(serverListener, never()).onDataRead(any(ChannelHandlerContext.class), anyInt(),
                any(ByteBuf.class), anyInt(), anyBoolean());
    }

    private void awaitRequests() throws Exception {
        assertTrue(requestLatch.await(WAIT_TIME_SECONDS, SECONDS));
        if (trailersLatch != null) {
            assertTrue(trailersLatch.await(WAIT_TIME_SECONDS, SECONDS));
        }
        assertTrue(serverSettingsAckLatch.await(WAIT_TIME_SECONDS, SECONDS));
    }

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

    private ChannelPromise newPromise() {
        return ctx().newPromise();
    }
}
