package org.jooby.internal.netty;

import static org.easymock.EasyMock.eq;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.isA;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;

import org.jooby.internal.netty.NettyPipeline.Http2OrHttpHandler;
import org.jooby.internal.netty.NettyPipeline.Http2PrefaceOrHttpHandler;
import org.jooby.spi.HttpHandler;
import org.jooby.test.MockUnit;
import org.jooby.test.MockUnit.Block;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import com.typesafe.config.ConfigValueFactory;

import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.HttpServerUpgradeHandler;
import io.netty.handler.codec.http.HttpServerUpgradeHandler.SourceCodec;
import io.netty.handler.codec.http.HttpServerUpgradeHandler.UpgradeCodecFactory;
import io.netty.handler.codec.http2.DefaultHttp2Connection;
import io.netty.handler.codec.http2.Http2FrameLogger;
import io.netty.handler.codec.http2.Http2ServerUpgradeCodec;
import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandler;
import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandlerBuilder;
import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapter;
import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapterBuilder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.util.concurrent.EventExecutorGroup;

@RunWith(PowerMockRunner.class)
@PrepareForTest({NettyPipeline.class, SslContext.class, HttpServerCodec.class,
    IdleStateHandler.class, InboundHttp2ToHttpAdapterBuilder.class,
    HttpToHttp2ConnectionHandlerBuilder.class })
public class NettyPipelineTest {

  private Block pipeline = unit -> {
    SocketChannel channel = unit.get(SocketChannel.class);
    expect(channel.pipeline()).andReturn(unit.get(ChannelPipeline.class));
  };

  private Block ctxpipeline = unit -> {
    ChannelHandlerContext channel = unit.get(ChannelHandlerContext.class);
    expect(channel.pipeline()).andReturn(unit.get(ChannelPipeline.class));
  };

  private Block ssl = unit -> {
    ByteBufAllocator bufalloc = unit.mock(ByteBufAllocator.class);

    SocketChannel channel = unit.get(SocketChannel.class);
    expect(channel.alloc()).andReturn(bufalloc);

    SslHandler handler = unit.mock(SslHandler.class);

    SslContext sslContext = unit.get(SslContext.class);
    expect(sslContext.newHandler(bufalloc)).andReturn(handler);

    ChannelPipeline pipeline = unit.get(ChannelPipeline.class);
    expect(pipeline.addLast("ssl", handler)).andReturn(pipeline);
  };

  private Block sslContext = unit -> {
    SslContext sslcontext = unit.powerMock(SslContext.class);
    unit.registerMock(SslContext.class, sslcontext);
  };

  private Block http2OrHttp = unit -> {
    ChannelPipeline pipeline = unit.get(ChannelPipeline.class);
    expect(pipeline.addLast(eq("h1.1/h2"), unit.capture(Http2OrHttpHandler.class)))
        .andReturn(pipeline);
  };

  @Test
  public void https1_1() throws Exception {
    Config conf = conf(false, 123, 234, 345, 456, 567L);
    new MockUnit(EventExecutorGroup.class, HttpHandler.class, SocketChannel.class,
        ChannelPipeline.class, ChannelHandlerContext.class)
            .expect(sslContext)
            .expect(pipeline)
            .expect(ssl)
            .expect(http2OrHttp)
            .expect(ctxpipeline)
            .expect(http1Codec())
            .expect(idle(567))
            .expect(aggregator(456))
            .expect(jooby(conf))
            .run(unit -> {
              new NettyPipeline(unit.get(EventExecutorGroup.class), unit.get(HttpHandler.class),
                  conf, unit.get(SslContext.class))
                      .initChannel(unit.get(SocketChannel.class));
            }, unit -> {
              Http2OrHttpHandler handler = unit.captured(Http2OrHttpHandler.class).iterator()
                  .next();
              handler.configurePipeline(unit.get(ChannelHandlerContext.class), "http/1.1");
            });
  }

  @Test
  public void http1_1() throws Exception {
    Config conf = conf(false, 123, 234, 345, 456, 567L);
    new MockUnit(EventExecutorGroup.class, HttpHandler.class, SocketChannel.class,
        ChannelPipeline.class, ChannelHandlerContext.class)
            .expect(pipeline)
            .expect(http1Codec())
            .expect(idle(567))
            .expect(aggregator(456))
            .expect(jooby(conf))
            .run(unit -> {
              new NettyPipeline(unit.get(EventExecutorGroup.class), unit.get(HttpHandler.class),
                  conf, null)
                      .initChannel(unit.get(SocketChannel.class));
            });
  }

  @Test
  public void h2cDirect() throws Exception {
    Config conf = conf(true, 123, 234, 345, 456, 567L);
    new MockUnit(EventExecutorGroup.class, HttpHandler.class, SocketChannel.class,
        ChannelPipeline.class, ChannelHandlerContext.class)
            .expect(pipeline)
            .expect(unit -> {
              ChannelPipeline pipeline = unit.get(ChannelPipeline.class);
              expect(pipeline.addLast(eq("h2c"), unit.capture(Http2PrefaceOrHttpHandler.class)))
                  .andReturn(pipeline);
            })
            .expect(ctxpipeline)
            .expect(ctxpipeline)
            .expect(h2(456, (u, h2) -> {
              ChannelPipeline p = u.get(ChannelPipeline.class);
              expect(p.addAfter(null, "h2", h2)).andReturn(p);
              ChannelHandlerContext ctx = u.get(ChannelHandlerContext.class);
              expect(ctx.name()).andReturn("h2");
              expect(p.context(h2)).andReturn(ctx);
              expect(p.remove(isA(Http2PrefaceOrHttpHandler.class))).andReturn(p);
            }))
            .expect(idle(567))
            .expect(aggregator(456))
            .expect(jooby(conf))
            .run(unit -> {
              new NettyPipeline(unit.get(EventExecutorGroup.class), unit.get(HttpHandler.class),
                  conf, null)
                      .initChannel(unit.get(SocketChannel.class));
            }, unit -> {
              Http2PrefaceOrHttpHandler handler = unit.captured(Http2PrefaceOrHttpHandler.class)
                  .iterator()
                  .next();
              handler.decode(unit.get(ChannelHandlerContext.class),
                  Unpooled.wrappedBuffer("PRI HTTP".getBytes(StandardCharsets.UTF_8)), null);
            });
  }

  @Test
  public void h2prefaceIgnored() throws Exception {
    Config conf = conf(true, 123, 234, 345, 456, 567L);
    new MockUnit(EventExecutorGroup.class, HttpHandler.class, SocketChannel.class,
        ChannelPipeline.class, ChannelHandlerContext.class)
            .expect(pipeline)
            .expect(unit -> {
              ChannelPipeline pipeline = unit.get(ChannelPipeline.class);
              expect(pipeline.addLast(eq("h2c"), unit.capture(Http2PrefaceOrHttpHandler.class)))
                  .andReturn(pipeline);
            })
            .expect(idle(567))
            .expect(aggregator(456))
            .expect(jooby(conf))
            .run(unit -> {
              new NettyPipeline(unit.get(EventExecutorGroup.class), unit.get(HttpHandler.class),
                  conf, null)
                      .initChannel(unit.get(SocketChannel.class));
            }, unit -> {
              Http2PrefaceOrHttpHandler handler = unit.captured(Http2PrefaceOrHttpHandler.class)
                  .iterator()
                  .next();
              handler.decode(unit.get(ChannelHandlerContext.class),
                  Unpooled.wrappedBuffer("123".getBytes(StandardCharsets.UTF_8)), null);
            });
  }

  @Test
  public void httpToh2c() throws Exception {
    Config conf = conf(true, 123, 234, 345, 456, 567L);
    new MockUnit(EventExecutorGroup.class, HttpHandler.class, SocketChannel.class,
        ChannelPipeline.class, ChannelHandlerContext.class)
            .expect(pipeline)
            .expect(unit -> {
              ChannelPipeline pipeline = unit.get(ChannelPipeline.class);
              expect(pipeline.addLast(eq("h2c"), unit.capture(Http2PrefaceOrHttpHandler.class)))
                  .andReturn(pipeline);
            })
            .expect(ctxpipeline)
            .expect(ctxpipeline)
            .expect(http1Codec((p, h) -> {
              expect(p.addAfter(null, "codec", h)).andReturn(p);
            }))
            .expect(unit -> {
              ChannelHandlerContext ctx = unit.get(ChannelHandlerContext.class);
              expect(ctx.name()).andReturn("codec");
              ChannelPipeline p = unit.get(ChannelPipeline.class);
              expect(p.context(unit.get(HttpServerCodec.class))).andReturn(ctx);
            })
            .expect(unit -> {
              ChannelHandlerContext ctx = unit.get(ChannelHandlerContext.class);
              expect(ctx.name()).andReturn("h2upgrade");

              ChannelPipeline p = unit.get(ChannelPipeline.class);
              expect(p.addAfter(eq("codec"), eq("h2upgrade"),
                  unit.capture(HttpServerUpgradeHandler.class)))
                      .andReturn(p);
              expect(p.context(isA(HttpServerUpgradeHandler.class))).andReturn(ctx);

              expect(p.remove(isA(Http2PrefaceOrHttpHandler.class))).andReturn(p);
            })
            .expect(idle(567))
            .expect(aggregator(456))
            .expect(jooby(conf))
            .run(unit -> {
              new NettyPipeline(unit.get(EventExecutorGroup.class), unit.get(HttpHandler.class),
                  conf, null)
                      .initChannel(unit.get(SocketChannel.class));
            }, unit -> {
              Http2PrefaceOrHttpHandler handler = unit.captured(Http2PrefaceOrHttpHandler.class)
                  .iterator()
                  .next();
              handler.decode(unit.get(ChannelHandlerContext.class),
                  Unpooled.wrappedBuffer("GET HTTP".getBytes(StandardCharsets.UTF_8)), null);
            });
  }

  @Test
  public void httpToh2cIgnoreUpgrade() throws Exception {
    Config conf = conf(true, 123, 234, 345, 456, 567L);
    new MockUnit(EventExecutorGroup.class, HttpHandler.class, SocketChannel.class,
        ChannelPipeline.class, ChannelHandlerContext.class)
            .expect(pipeline)
            .expect(unit -> {
              ChannelPipeline pipeline = unit.get(ChannelPipeline.class);
              expect(pipeline.addLast(eq("h2c"), unit.capture(Http2PrefaceOrHttpHandler.class)))
                  .andReturn(pipeline);
            })
            .expect(ctxpipeline)
            .expect(ctxpipeline)
            .expect(http1Codec((p, h) -> {
              expect(p.addAfter(null, "codec", h)).andReturn(p);
            }))
            .expect(unit -> {
              ChannelHandlerContext ctx = unit.get(ChannelHandlerContext.class);
              expect(ctx.name()).andReturn("codec");
              ChannelPipeline p = unit.get(ChannelPipeline.class);
              expect(p.context(unit.get(HttpServerCodec.class))).andReturn(ctx);
            })
            .expect(unit -> {
              ChannelHandlerContext ctx = unit.get(ChannelHandlerContext.class);
              expect(ctx.name()).andReturn("h2upgrade");

              HttpServerCodec http1Codec = unit.get(HttpServerCodec.class);

              HttpServerUpgradeHandler h = unit.constructor(HttpServerUpgradeHandler.class)
                  .args(SourceCodec.class, UpgradeCodecFactory.class, int.class)
                  .build(eq(http1Codec), unit.capture(UpgradeCodecFactory.class), eq(456));

              ChannelPipeline p = unit.get(ChannelPipeline.class);
              expect(p.addAfter("codec", "h2upgrade", h)).andReturn(p);
              expect(p.context(isA(HttpServerUpgradeHandler.class))).andReturn(ctx);

              expect(p.remove(isA(Http2PrefaceOrHttpHandler.class))).andReturn(p);
            })
            .expect(idle(567))
            .expect(aggregator(456))
            .expect(jooby(conf))
            .run(unit -> {
              new NettyPipeline(unit.get(EventExecutorGroup.class), unit.get(HttpHandler.class),
                  conf, null)
                      .initChannel(unit.get(SocketChannel.class));
            }, unit -> {
              Http2PrefaceOrHttpHandler handler = unit.captured(Http2PrefaceOrHttpHandler.class)
                  .iterator()
                  .next();
              handler.decode(unit.get(ChannelHandlerContext.class),
                  Unpooled.wrappedBuffer("GET HTTP".getBytes(StandardCharsets.UTF_8)), null);
            }, unit -> {
              UpgradeCodecFactory u = unit.captured(UpgradeCodecFactory.class).iterator().next();
              assertEquals(null, u.newUpgradeCodec("http/1.1"));
            });
  }

  @Test
  public void httpToh2cSuccessUpgrade() throws Exception {
    Config conf = conf(true, 123, 234, 345, 456, 567L);
    new MockUnit(EventExecutorGroup.class, HttpHandler.class, SocketChannel.class,
        ChannelPipeline.class, ChannelHandlerContext.class)
            .expect(pipeline)
            .expect(unit -> {
              ChannelPipeline pipeline = unit.get(ChannelPipeline.class);
              expect(pipeline.addLast(eq("h2c"), unit.capture(Http2PrefaceOrHttpHandler.class)))
                  .andReturn(pipeline);
            })
            .expect(ctxpipeline)
            .expect(ctxpipeline)
            .expect(http1Codec((p, h) -> {
              expect(p.addAfter(null, "codec", h)).andReturn(p);
            }))
            .expect(unit -> {
              ChannelHandlerContext ctx = unit.get(ChannelHandlerContext.class);
              expect(ctx.name()).andReturn("codec");
              ChannelPipeline p = unit.get(ChannelPipeline.class);
              expect(p.context(unit.get(HttpServerCodec.class))).andReturn(ctx);
            })
            .expect(unit -> {
              ChannelHandlerContext ctx = unit.get(ChannelHandlerContext.class);
              expect(ctx.name()).andReturn("h2upgrade");

              HttpServerCodec http1Codec = unit.get(HttpServerCodec.class);

              HttpServerUpgradeHandler h = unit.constructor(HttpServerUpgradeHandler.class)
                  .args(SourceCodec.class, UpgradeCodecFactory.class, int.class)
                  .build(eq(http1Codec), unit.capture(UpgradeCodecFactory.class), eq(456));

              ChannelPipeline p = unit.get(ChannelPipeline.class);
              expect(p.addAfter("codec", "h2upgrade", h)).andReturn(p);
              expect(p.context(isA(HttpServerUpgradeHandler.class))).andReturn(ctx);

              expect(p.remove(isA(Http2PrefaceOrHttpHandler.class))).andReturn(p);
            })
            .expect(idle(567))
            .expect(aggregator(456))
            .expect(jooby(conf))
            .run(unit -> {
              new NettyPipeline(unit.get(EventExecutorGroup.class), unit.get(HttpHandler.class),
                  conf, null)
                      .initChannel(unit.get(SocketChannel.class));
            }, unit -> {
              Http2PrefaceOrHttpHandler handler = unit.captured(Http2PrefaceOrHttpHandler.class)
                  .iterator()
                  .next();
              handler.decode(unit.get(ChannelHandlerContext.class),
                  Unpooled.wrappedBuffer("GET HTTP".getBytes(StandardCharsets.UTF_8)), null);
            }, unit -> {
              UpgradeCodecFactory u = unit.captured(UpgradeCodecFactory.class).iterator().next();
              assertTrue(u.newUpgradeCodec("h2c") instanceof Http2ServerUpgradeCodec);
            });
  }

  private Block http1Codec() {
    return http1Codec((p, h) -> {
      expect(p.addLast("codec", h)).andReturn(p);
    });
  }

  private Block http1Codec(final BiConsumer<ChannelPipeline, HttpServerCodec> callback) {
    return unit -> {
      HttpServerCodec codec = unit.constructor(HttpServerCodec.class)
          .build(123, 234, 345, false);
      unit.registerMock(HttpServerCodec.class, codec);

      ChannelPipeline pipeline = unit.get(ChannelPipeline.class);
      callback.accept(pipeline, codec);
    };
  }

  @Test
  public void h2() throws Exception {
    Config conf = conf(true, 123, 234, 345, 456, 567L);
    new MockUnit(EventExecutorGroup.class, HttpHandler.class, SocketChannel.class,
        ChannelPipeline.class, ChannelHandlerContext.class)
            .expect(sslContext)
            .expect(pipeline)
            .expect(ssl)
            .expect(http2OrHttp)
            .expect(ctxpipeline)
            .expect(h2(456))
            .expect(idle(567))
            .expect(jooby(conf))
            .run(unit -> {
              new NettyPipeline(unit.get(EventExecutorGroup.class), unit.get(HttpHandler.class),
                  conf, unit.get(SslContext.class))
                      .initChannel(unit.get(SocketChannel.class));
            }, unit -> {
              Http2OrHttpHandler handler = unit.captured(Http2OrHttpHandler.class).iterator()
                  .next();
              handler.configurePipeline(unit.get(ChannelHandlerContext.class), "h2");
            });
  }

  @Test
  public void https1_1_noTimeout() throws Exception {
    Config conf = conf(false, 123, 234, 345, 456, -1);
    new MockUnit(EventExecutorGroup.class, HttpHandler.class, SocketChannel.class,
        ChannelPipeline.class, ChannelHandlerContext.class)
            .expect(sslContext)
            .expect(pipeline)
            .expect(ssl)
            .expect(http2OrHttp)
            .expect(ctxpipeline)
            .expect(http1Codec())
            .expect(aggregator(456))
            .expect(jooby(conf))
            .run(unit -> {
              new NettyPipeline(unit.get(EventExecutorGroup.class), unit.get(HttpHandler.class),
                  conf, unit.get(SslContext.class))
                      .initChannel(unit.get(SocketChannel.class));
            }, unit -> {
              Http2OrHttpHandler handler = unit.captured(Http2OrHttpHandler.class).iterator()
                  .next();
              handler.configurePipeline(unit.get(ChannelHandlerContext.class), "http/1.1");
            });
  }

  @Test
  public void unknownProtocol() throws Exception {
    Config conf = conf(false, 123, 234, 345, 456, 567L);
    new MockUnit(EventExecutorGroup.class, HttpHandler.class, SocketChannel.class,
        ChannelPipeline.class, ChannelHandlerContext.class)
            .expect(sslContext)
            .expect(pipeline)
            .expect(ssl)
            .expect(http2OrHttp)
            .run(unit -> {
              new NettyPipeline(unit.get(EventExecutorGroup.class), unit.get(HttpHandler.class),
                  conf, unit.get(SslContext.class))
                      .initChannel(unit.get(SocketChannel.class));
            }, unit -> {
              Http2OrHttpHandler handler = unit.captured(Http2OrHttpHandler.class).iterator()
                  .next();
              try {
                handler.configurePipeline(unit.get(ChannelHandlerContext.class), "h2");
                fail();
              } catch (IllegalStateException x) {
                assertEquals("Unknown protocol: h2", x.getMessage());
              }
            });
  }

  private Block jooby(final Config conf) {
    return unit -> {
      NettyHandler handler = unit.constructor(NettyHandler.class)
          .build(unit.get(HttpHandler.class), conf.getString("application.tmpdir"),
              conf.getBytes("server.http.ResponseBufferSize").intValue(),
              conf.getBytes("server.ws.MaxTextMessageSize").intValue());
      unit.registerMock(NettyHandler.class, handler);

      ChannelPipeline pipeline = unit.get(ChannelPipeline.class);
      expect(pipeline.addLast(unit.get(EventExecutorGroup.class), "jooby", handler))
          .andReturn(pipeline);
    };
  }

  private Block aggregator(final int len) {
    return unit -> {
      HttpObjectAggregator aggregator = unit.constructor(HttpObjectAggregator.class)
          .build(len);
      unit.registerMock(HttpObjectAggregator.class, aggregator);

      ChannelPipeline pipeline = unit.get(ChannelPipeline.class);
      expect(pipeline.addLast("aggregator", aggregator)).andReturn(pipeline);
    };
  }

  private Block h2(final int l) {
    return h2(l, (u, h2) -> {
      ChannelPipeline pipeline = u.get(ChannelPipeline.class);
      expect(pipeline.addLast("h2", h2)).andReturn(pipeline);
    });
  }

  private Block h2(final int l,
      final BiConsumer<MockUnit, HttpToHttp2ConnectionHandler> handler) {
    return unit -> {

      DefaultHttp2Connection connection = unit.constructor(DefaultHttp2Connection.class)
          .build(true);

      InboundHttp2ToHttpAdapterBuilder builder = unit
          .constructor(InboundHttp2ToHttpAdapterBuilder.class)
          .build(connection);

      InboundHttp2ToHttpAdapter adapter = unit.mock(InboundHttp2ToHttpAdapter.class);
      Http2FrameLogger logger = unit.constructor(Http2FrameLogger.class)
          .build(LogLevel.DEBUG);

      expect(builder.propagateSettings(false)).andReturn(builder);
      expect(builder.validateHttpHeaders(false)).andReturn(builder);
      expect(builder.maxContentLength(l)).andReturn(builder);
      expect(builder.build()).andReturn(adapter);

      HttpToHttp2ConnectionHandler h2 = unit.mock(HttpToHttp2ConnectionHandler.class);

      HttpToHttp2ConnectionHandlerBuilder h2builder = unit
          .constructor(HttpToHttp2ConnectionHandlerBuilder.class)
          .build();
      expect(h2builder.frameListener(adapter)).andReturn(h2builder);
      expect(h2builder.frameLogger(logger)).andReturn(h2builder);
      expect(h2builder.connection(connection)).andReturn(h2builder);
      expect(h2builder.build()).andReturn(h2);

      handler.accept(unit, h2);
    };
  }

  private Block idle(final long timeout) {
    return unit -> {
      IdleStateHandler idle = unit.constructor(IdleStateHandler.class)
          .build(0L, 0L, timeout, TimeUnit.MILLISECONDS);
      unit.registerMock(IdleStateHandler.class, idle);

      ChannelPipeline pipeline = unit.get(ChannelPipeline.class);
      expect(pipeline.addLast("timeout", idle)).andReturn(pipeline);
    };
  }

  private Config conf(final boolean http2, final int i, final int j, final int k, final int l,
      final long m) {
    return ConfigFactory.empty()
        .withValue("netty.http.MaxInitialLineLength", ConfigValueFactory.fromAnyRef(i))
        .withValue("netty.http.MaxHeaderSize", ConfigValueFactory.fromAnyRef(j))
        .withValue("netty.http.MaxChunkSize", ConfigValueFactory.fromAnyRef(k))
        .withValue("netty.http.MaxContentLength", ConfigValueFactory.fromAnyRef(l))
        .withValue("netty.http.IdleTimeout", ConfigValueFactory.fromAnyRef(m))
        .withValue("server.http2.enabled", ConfigValueFactory.fromAnyRef(http2))
        .withValue("server.http.ResponseBufferSize", ConfigValueFactory.fromAnyRef("16k"))
        .withValue("server.ws.MaxTextMessageSize", ConfigValueFactory.fromAnyRef("16k"))
        .withValue("server.ws.MaxBinaryMessageSize", ConfigValueFactory.fromAnyRef("16k"))
        .withValue("application.tmpdir", ConfigValueFactory.fromAnyRef("target"));
  }
}
