/*
 * Copyright 2019 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.util.internal;

import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.buffer.UnpooledByteBufAllocator;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioDatagramChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.ssl.SslHandshakeCompletionEvent;
import io.netty.handler.ssl.SslProvider;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import io.netty.handler.ssl.util.SelfSignedCertificate;
import io.netty.resolver.dns.DnsNameResolverBuilder;
import io.netty.resolver.dns.DnsServerAddressStreamProviders;
import io.netty.util.HashedWheelTimer;
import io.netty.util.ReferenceCountUtil;
import io.netty.util.concurrent.DefaultThreadFactory;
import io.netty.util.concurrent.EventExecutor;
import io.netty.util.concurrent.FastThreadLocalThread;
import io.netty.util.concurrent.GlobalEventExecutor;
import io.netty.util.concurrent.ImmediateEventExecutor;
import io.netty.util.concurrent.ImmediateExecutor;
import io.netty.util.concurrent.ScheduledFuture;
import io.netty.util.concurrent.SingleThreadEventExecutor;
import io.netty.util.internal.Hidden.NettyBlockHoundIntegration;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import reactor.blockhound.BlockHound;
import reactor.blockhound.BlockingOperationError;
import reactor.blockhound.integration.BlockHoundIntegration;

import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.ServiceLoader;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;

import static io.netty.buffer.Unpooled.wrappedBuffer;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

public class NettyBlockHoundIntegrationTest {

    @BeforeAll
    public static void setUpClass() {
        BlockHound.install();
    }

    @Test
    public void testServiceLoader() {
        for (BlockHoundIntegration integration : ServiceLoader.load(BlockHoundIntegration.class)) {
            if (integration instanceof NettyBlockHoundIntegration) {
                return;
            }
        }

        fail("NettyBlockHoundIntegration cannot be loaded with ServiceLoader");
    }

    @Test
    public void testBlockingCallsInNettyThreads() throws Exception {
        final FutureTask<Void> future = new FutureTask<>(() -> {
            Thread.sleep(0);
            return null;
        });
        GlobalEventExecutor.INSTANCE.execute(future);

        try {
            future.get(5, TimeUnit.SECONDS);
            fail("Expected an exception due to a blocking call but none was thrown");
        } catch (ExecutionException e) {
            assertThat(e.getCause(), Matchers.instanceOf(BlockingOperationError.class));
        }
    }

    @Test
    @Timeout(value = 5000, unit = TimeUnit.MILLISECONDS)
    public void testGlobalEventExecutorTakeTask() throws InterruptedException {
        testEventExecutorTakeTask(GlobalEventExecutor.INSTANCE);
    }

    @Test
    @Timeout(value = 5000, unit = TimeUnit.MILLISECONDS)
    public void testSingleThreadEventExecutorTakeTask() throws InterruptedException {
        SingleThreadEventExecutor executor =
                new SingleThreadEventExecutor(null, new DefaultThreadFactory("test"), true) {
                    @Override
                    protected void run() {
                        while (!confirmShutdown()) {
                            Runnable task = takeTask();
                            if (task != null) {
                                task.run();
                            }
                        }
                    }
                };
        testEventExecutorTakeTask(executor);
    }

    private static void testEventExecutorTakeTask(EventExecutor eventExecutor) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(1);
        ScheduledFuture<?> f = eventExecutor.schedule(latch::countDown, 10, TimeUnit.MILLISECONDS);
        f.sync();
        latch.await();
    }

    @Test
    @Timeout(value = 5000, unit = TimeUnit.MILLISECONDS)
    public void testSingleThreadEventExecutorAddTask() throws Exception {
        TestLinkedBlockingQueue<Runnable> taskQueue = new TestLinkedBlockingQueue<>();
        SingleThreadEventExecutor executor =
                new SingleThreadEventExecutor(null, new DefaultThreadFactory("test"), true) {
                    @Override
                    protected Queue<Runnable> newTaskQueue(int maxPendingTasks) {
                        return taskQueue;
                    }

                    @Override
                    protected void run() {
                        while (!confirmShutdown()) {
                            Runnable task = takeTask();
                            if (task != null) {
                                task.run();
                            }
                        }
                    }
                };
        taskQueue.emulateContention();
        CountDownLatch latch = new CountDownLatch(1);
        executor.submit(() -> {
            executor.execute(() -> { }); // calls addTask
            latch.countDown();
        });
        taskQueue.waitUntilContented();
        taskQueue.removeContention();
        latch.await();
    }

    @Test
    void permittingBlockingCallsInFastThreadLocalThreadSubclass() throws Exception {
        final FutureTask<Void> future = new FutureTask<>(() -> {
            Thread.sleep(0);
            return null;
        });
        FastThreadLocalThread thread = new FastThreadLocalThread(future) {
            @Override
            public boolean permitBlockingCalls() {
                return true; // The Thread.sleep(0) call should not be flagged because we allow blocking calls.
            }
        };
        thread.start();
        future.get(5, TimeUnit.SECONDS);
        thread.join();
    }

    @Test
    @Timeout(value = 5000, unit = TimeUnit.MILLISECONDS)
    public void testHashedWheelTimerStartStop() throws Exception {
        HashedWheelTimer timer = new HashedWheelTimer();
        Future<?> futureStart = GlobalEventExecutor.INSTANCE.submit(timer::start);
        futureStart.get(5, TimeUnit.SECONDS);
        Future<?> futureStop = GlobalEventExecutor.INSTANCE.submit(timer::stop);
        futureStop.get(5, TimeUnit.SECONDS);
    }

    // Tests copied from io.netty.handler.ssl.SslHandlerTest
    @Test
    public void testHandshakeWithExecutorThatExecuteDirectory() throws Exception {
        testHandshakeWithExecutor(Runnable::run, "TLSv1.2");
    }

    @Test
    public void testHandshakeWithExecutorThatExecuteDirectoryTLSv13() throws Exception {
        assumeTrue(SslProvider.isTlsv13Supported(SslProvider.JDK));
        testHandshakeWithExecutor(Runnable::run, "TLSv1.3");
    }

    @Test
    public void testHandshakeWithImmediateExecutor() throws Exception {
        testHandshakeWithExecutor(ImmediateExecutor.INSTANCE, "TLSv1.2");
    }

    @Test
    public void testHandshakeWithImmediateExecutorTLSv13() throws Exception {
        assumeTrue(SslProvider.isTlsv13Supported(SslProvider.JDK));
        testHandshakeWithExecutor(ImmediateExecutor.INSTANCE, "TLSv1.3");
    }

    @Test
    public void testHandshakeWithImmediateEventExecutor() throws Exception {
        testHandshakeWithExecutor(ImmediateEventExecutor.INSTANCE, "TLSv1.2");
    }

    @Test
    public void testHandshakeWithImmediateEventExecutorTLSv13() throws Exception {
        assumeTrue(SslProvider.isTlsv13Supported(SslProvider.JDK));
        testHandshakeWithExecutor(ImmediateEventExecutor.INSTANCE, "TLSv1.3");
    }

    @Test
    public void testHandshakeWithExecutor() throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        try {
            testHandshakeWithExecutor(executorService, "TLSv1.2");
        } finally {
            executorService.shutdown();
        }
    }

    @Test
    public void testHandshakeWithExecutorTLSv13() throws Exception {
        assumeTrue(SslProvider.isTlsv13Supported(SslProvider.JDK));
        ExecutorService executorService = Executors.newCachedThreadPool();
        try {
            testHandshakeWithExecutor(executorService, "TLSv1.3");
        } finally {
            executorService.shutdown();
        }
    }

    @Test
    public void testTrustManagerVerifyJDK() throws Exception {
        testTrustManagerVerify(SslProvider.JDK, "TLSv1.2");
    }

    @Test
    public void testTrustManagerVerifyTLSv13JDK() throws Exception {
        assumeTrue(SslProvider.isTlsv13Supported(SslProvider.JDK));
        testTrustManagerVerify(SslProvider.JDK, "TLSv1.3");
    }

    @Test
    public void testTrustManagerVerifyOpenSSL() throws Exception {
        testTrustManagerVerify(SslProvider.OPENSSL, "TLSv1.2");
    }

    @Test
    public void testTrustManagerVerifyTLSv13OpenSSL() throws Exception {
        assumeTrue(SslProvider.isTlsv13Supported(SslProvider.OPENSSL));
        testTrustManagerVerify(SslProvider.OPENSSL, "TLSv1.3");
    }

    @Test
    public void testSslHandlerWrapAllowsBlockingCalls() throws Exception {
        final SslContext sslClientCtx =
                SslContextBuilder.forClient()
                                 .trustManager(InsecureTrustManagerFactory.INSTANCE)
                                 .sslProvider(SslProvider.JDK)
                                 .build();
        final SslHandler sslHandler = sslClientCtx.newHandler(UnpooledByteBufAllocator.DEFAULT);
        final EventLoopGroup group = new NioEventLoopGroup();
        final CountDownLatch activeLatch = new CountDownLatch(1);
        final AtomicReference<Throwable> error = new AtomicReference<>();

        Channel sc = null;
        Channel cc = null;
        try {
            sc = new ServerBootstrap()
                    .group(group)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInboundHandlerAdapter())
                    .bind(new InetSocketAddress(0))
                    .syncUninterruptibly()
                    .channel();

            cc = new Bootstrap()
                    .group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<Channel>() {

                        @Override
                        protected void initChannel(Channel ch) {
                            ch.pipeline().addLast(sslHandler);
                            ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {

                                @Override
                                public void channelActive(ChannelHandlerContext ctx) {
                                    activeLatch.countDown();
                                }

                                @Override
                                public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
                                    if (evt instanceof SslHandshakeCompletionEvent &&
                                            ((SslHandshakeCompletionEvent) evt).cause() != null) {
                                        Throwable cause = ((SslHandshakeCompletionEvent) evt).cause();
                                        cause.printStackTrace();
                                        error.set(cause);
                                    }
                                    ctx.fireUserEventTriggered(evt);
                                }
                            });
                        }
                    })
                    .connect(sc.localAddress())
                    .addListener((ChannelFutureListener) future ->
                        future.channel().writeAndFlush(wrappedBuffer(new byte [] { 1, 2, 3, 4 })))
                    .syncUninterruptibly()
                    .channel();

            assertTrue(activeLatch.await(5, TimeUnit.SECONDS));
            assertNull(error.get());
        } finally {
            if (cc != null) {
                cc.close().syncUninterruptibly();
            }
            if (sc != null) {
                sc.close().syncUninterruptibly();
            }
            group.shutdownGracefully();
            ReferenceCountUtil.release(sslClientCtx);
        }
    }

    @Test
    @Timeout(value = 5000, unit = TimeUnit.MILLISECONDS)
    public void pooledBufferAllocation() throws Exception {
        AtomicLong iterationCounter = new AtomicLong();
        PooledByteBufAllocator allocator = PooledByteBufAllocator.DEFAULT;
        FutureTask<Void> task = new FutureTask<>(() -> {
            List<ByteBuf> buffers = new ArrayList<>();
            long count;
            do {
                count = iterationCounter.get();
            } while (count == 0);
            for (int i = 0; i < 13; i++) {
                int size = 8 << i;
                buffers.add(allocator.ioBuffer(size, size));
            }
            for (ByteBuf buffer : buffers) {
                buffer.release();
            }
            return null;
        });
        FastThreadLocalThread thread = new FastThreadLocalThread(task);
        thread.start();
        do {
            allocator.dumpStats(); // This will take internal pool locks and we'll race with the thread.
            iterationCounter.set(1);
        } while (thread.isAlive());
        thread.join();
        task.get();
    }

    @Test
    @Timeout(value = 5000, unit = TimeUnit.MILLISECONDS)
    public void testUnixResolverDnsServerAddressStreamProvider_Parse() throws InterruptedException {
        doTestParseResolverFilesAllowsBlockingCalls(DnsServerAddressStreamProviders::unixDefault);
    }

    @Test
    @Timeout(value = 5000, unit = TimeUnit.MILLISECONDS)
    public void testHostsFileParser_Parse() throws InterruptedException {
        doTestParseResolverFilesAllowsBlockingCalls(DnsNameResolverBuilder::new);
    }

    @Test
    @Timeout(value = 5000, unit = TimeUnit.MILLISECONDS)
    public void testUnixResolverDnsServerAddressStreamProvider_ParseEtcResolverSearchDomainsAndOptions()
            throws InterruptedException {
        NioEventLoopGroup group = new NioEventLoopGroup();
        try {
            DnsNameResolverBuilder builder = new DnsNameResolverBuilder(group.next())
                    .datagramChannelFactory(NioDatagramChannel::new);
            doTestParseResolverFilesAllowsBlockingCalls(builder::build);
        } finally {
            group.shutdownGracefully();
        }
    }

    private static void doTestParseResolverFilesAllowsBlockingCalls(Callable<Object> callable)
            throws InterruptedException {
        SingleThreadEventExecutor executor =
                new SingleThreadEventExecutor(null, new DefaultThreadFactory("test"), true) {
                    @Override
                    protected void run() {
                        while (!confirmShutdown()) {
                            Runnable task = takeTask();
                            if (task != null) {
                                task.run();
                            }
                        }
                    }
                };
        try {
            CountDownLatch latch = new CountDownLatch(1);
            List<Object> result = new ArrayList<>();
            List<Throwable> error = new ArrayList<>();
            executor.execute(() -> {
                try {
                    result.add(callable.call());
                } catch (Throwable t) {
                    error.add(t);
                }
                latch.countDown();
            });
            latch.await();
            assertEquals(0, error.size());
            assertEquals(1, result.size());
        } finally {
            executor.shutdownGracefully();
        }
    }

    private static void testTrustManagerVerify(SslProvider provider, String tlsVersion) throws Exception {
        final SslContext sslClientCtx =
                SslContextBuilder.forClient()
                                 .sslProvider(provider)
                                 .protocols(tlsVersion)
                                 .trustManager(ResourcesUtil.getFile(
                                         NettyBlockHoundIntegrationTest.class, "mutual_auth_ca.pem"))
                                 .build();

        final SslContext sslServerCtx =
                SslContextBuilder.forServer(ResourcesUtil.getFile(
                        NettyBlockHoundIntegrationTest.class, "localhost_server.pem"),
                                            ResourcesUtil.getFile(
                                                    NettyBlockHoundIntegrationTest.class, "localhost_server.key"),
                                            null)
                                 .sslProvider(provider)
                                 .protocols(tlsVersion)
                                 .build();

        final SslHandler clientSslHandler = sslClientCtx.newHandler(UnpooledByteBufAllocator.DEFAULT);
        final SslHandler serverSslHandler = sslServerCtx.newHandler(UnpooledByteBufAllocator.DEFAULT);

        testHandshake(sslClientCtx, clientSslHandler, serverSslHandler);
    }

    private static void testHandshakeWithExecutor(Executor executor, String tlsVersion) throws Exception {
        final SslContext sslClientCtx = SslContextBuilder.forClient()
                .trustManager(InsecureTrustManagerFactory.INSTANCE)
                .sslProvider(SslProvider.JDK).protocols(tlsVersion).build();

        final SelfSignedCertificate cert = new SelfSignedCertificate();
        final SslContext sslServerCtx = SslContextBuilder.forServer(cert.key(), cert.cert())
                .sslProvider(SslProvider.JDK).protocols(tlsVersion).build();

        final SslHandler clientSslHandler = sslClientCtx.newHandler(UnpooledByteBufAllocator.DEFAULT, executor);
        final SslHandler serverSslHandler = sslServerCtx.newHandler(UnpooledByteBufAllocator.DEFAULT, executor);

        testHandshake(sslClientCtx, clientSslHandler, serverSslHandler);
    }

    private static void testHandshake(SslContext sslClientCtx, SslHandler clientSslHandler,
                                      SslHandler serverSslHandler) throws Exception {
        EventLoopGroup group = new NioEventLoopGroup();
        Channel sc = null;
        Channel cc = null;
        try {
            sc = new ServerBootstrap()
                    .group(group)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(serverSslHandler)
                    .bind(new InetSocketAddress(0)).syncUninterruptibly().channel();

            ChannelFuture future = new Bootstrap()
                    .group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<Channel>() {
                        @Override
                        protected void initChannel(Channel ch) {
                            ch.pipeline()
                              .addLast(clientSslHandler)
                              .addLast(new ChannelInboundHandlerAdapter() {

                                  @Override
                                  public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
                                      if (evt instanceof SslHandshakeCompletionEvent &&
                                              ((SslHandshakeCompletionEvent) evt).cause() != null) {
                                          ((SslHandshakeCompletionEvent) evt).cause().printStackTrace();
                                      }
                                      ctx.fireUserEventTriggered(evt);
                                  }
                              });
                        }
                    }).connect(sc.localAddress());
            cc = future.syncUninterruptibly().channel();

            clientSslHandler.handshakeFuture().await().sync();
            serverSslHandler.handshakeFuture().await().sync();
        } finally {
            if (cc != null) {
                cc.close().syncUninterruptibly();
            }
            if (sc != null) {
                sc.close().syncUninterruptibly();
            }
            group.shutdownGracefully();
            ReferenceCountUtil.release(sslClientCtx);
        }
    }

    private static class TestLinkedBlockingQueue<T> extends LinkedBlockingQueue<T> {

        private final ReentrantLock lock = new ReentrantLock();

        @Override
        public boolean offer(T t) {
            lock.lock();
            try {
                return super.offer(t);
            } finally {
                lock.unlock();
            }
        }

        void emulateContention() {
            lock.lock();
        }

        void waitUntilContented() throws InterruptedException {
            // wait until the lock gets contended
            while (lock.getQueueLength() == 0) {
                Thread.sleep(10L);
            }
        }

        void removeContention() {
            lock.unlock();
        }
    }
}
