/*
 * Copyright 2016 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.channel;

import io.netty.channel.embedded.EmbeddedChannel;
import org.junit.Test;

import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.ArrayDeque;
import java.util.Queue;

import static org.junit.Assert.*;

public class CombinedChannelDuplexHandlerTest {

    private static final Object MSG = new Object();
    private static final SocketAddress ADDRESS = new InetSocketAddress(0);

    private enum Event {
        REGISTERED,
        UNREGISTERED,
        ACTIVE,
        INACTIVE,
        CHANNEL_READ,
        CHANNEL_READ_COMPLETE,
        EXCEPTION_CAUGHT,
        USER_EVENT_TRIGGERED,
        CHANNEL_WRITABILITY_CHANGED,
        HANDLER_ADDED,
        HANDLER_REMOVED,
        BIND,
        CONNECT,
        WRITE,
        FLUSH,
        READ,
        REGISTER,
        DEREGISTER,
        CLOSE,
        DISCONNECT
    }

    @Test(expected = IllegalStateException.class)
    public void testInboundRemoveBeforeAdded() {
        CombinedChannelDuplexHandler<ChannelInboundHandler, ChannelOutboundHandler> handler =
                new CombinedChannelDuplexHandler<ChannelInboundHandler, ChannelOutboundHandler>(
                        new ChannelInboundHandlerAdapter(), new ChannelOutboundHandlerAdapter());
        handler.removeInboundHandler();
    }

    @Test(expected = IllegalStateException.class)
    public void testOutboundRemoveBeforeAdded() {
        CombinedChannelDuplexHandler<ChannelInboundHandler, ChannelOutboundHandler> handler =
                new CombinedChannelDuplexHandler<ChannelInboundHandler, ChannelOutboundHandler>(
                        new ChannelInboundHandlerAdapter(), new ChannelOutboundHandlerAdapter());
        handler.removeOutboundHandler();
    }

    @Test(expected = IllegalArgumentException.class)
    public void testInboundHandlerImplementsOutboundHandler() {
        new CombinedChannelDuplexHandler<ChannelInboundHandler, ChannelOutboundHandler>(
                new ChannelDuplexHandler(), new ChannelOutboundHandlerAdapter());
    }

    @Test(expected = IllegalArgumentException.class)
    public void testOutboundHandlerImplementsInbboundHandler() {
        new CombinedChannelDuplexHandler<ChannelInboundHandler, ChannelOutboundHandler>(
                new ChannelInboundHandlerAdapter(), new ChannelDuplexHandler());
    }

    @Test(expected = IllegalStateException.class)
    public void testInitNotCalledBeforeAdded() throws Exception {
        CombinedChannelDuplexHandler<ChannelInboundHandler, ChannelOutboundHandler> handler =
                new CombinedChannelDuplexHandler<ChannelInboundHandler, ChannelOutboundHandler>() { };
        handler.handlerAdded(null);
    }

    @Test
    public void testExceptionCaughtBothCombinedHandlers() {
        final Exception exception = new Exception();
        final Queue<ChannelHandler> queue = new ArrayDeque<ChannelHandler>();

        ChannelInboundHandler inboundHandler = new ChannelInboundHandlerAdapter() {
            @Override
            public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
                assertSame(exception, cause);
                queue.add(this);
                ctx.fireExceptionCaught(cause);
            }
        };
        ChannelOutboundHandler outboundHandler = new ChannelOutboundHandlerAdapter() {
            @Override
            public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
                assertSame(exception, cause);
                queue.add(this);
                ctx.fireExceptionCaught(cause);
            }
        };
        ChannelInboundHandler lastHandler = new ChannelInboundHandlerAdapter() {
            @Override
            public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
                assertSame(exception, cause);
                queue.add(this);
            }
        };
        EmbeddedChannel channel = new EmbeddedChannel(
                new CombinedChannelDuplexHandler<ChannelInboundHandler, ChannelOutboundHandler>(
                        inboundHandler, outboundHandler), lastHandler);
        channel.pipeline().fireExceptionCaught(exception);
        assertFalse(channel.finish());
        assertSame(inboundHandler, queue.poll());
        assertSame(outboundHandler, queue.poll());
        assertSame(lastHandler, queue.poll());
        assertTrue(queue.isEmpty());
    }

    @Test
    public void testInboundEvents() {
        final Queue<Event> queue = new ArrayDeque<Event>();

        ChannelInboundHandler inboundHandler = new ChannelInboundHandlerAdapter() {
            @Override
            public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
                queue.add(Event.HANDLER_ADDED);
            }

            @Override
            public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
                queue.add(Event.HANDLER_REMOVED);
            }

            @Override
            public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
                queue.add(Event.REGISTERED);
            }

            @Override
            public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
                queue.add(Event.UNREGISTERED);
            }

            @Override
            public void channelActive(ChannelHandlerContext ctx) throws Exception {
                queue.add(Event.ACTIVE);
            }

            @Override
            public void channelInactive(ChannelHandlerContext ctx) throws Exception {
                queue.add(Event.INACTIVE);
            }

            @Override
            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                queue.add(Event.CHANNEL_READ);
            }

            @Override
            public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
                queue.add(Event.CHANNEL_READ_COMPLETE);
            }

            @Override
            public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
                queue.add(Event.USER_EVENT_TRIGGERED);
            }

            @Override
            public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
                queue.add(Event.CHANNEL_WRITABILITY_CHANGED);
            }

            @Override
            public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
                queue.add(Event.EXCEPTION_CAUGHT);
            }
        };

        CombinedChannelDuplexHandler<ChannelInboundHandler, ChannelOutboundHandler> handler =
                new CombinedChannelDuplexHandler<ChannelInboundHandler, ChannelOutboundHandler>(
                inboundHandler, new ChannelOutboundHandlerAdapter());

        EmbeddedChannel channel = new EmbeddedChannel(handler);
        channel.pipeline().fireChannelWritabilityChanged();
        channel.pipeline().fireUserEventTriggered(MSG);
        channel.pipeline().fireChannelRead(MSG);
        channel.pipeline().fireChannelReadComplete();

        assertEquals(Event.HANDLER_ADDED, queue.poll());
        assertEquals(Event.REGISTERED, queue.poll());
        assertEquals(Event.ACTIVE, queue.poll());
        assertEquals(Event.CHANNEL_WRITABILITY_CHANGED, queue.poll());
        assertEquals(Event.USER_EVENT_TRIGGERED, queue.poll());
        assertEquals(Event.CHANNEL_READ, queue.poll());
        assertEquals(Event.CHANNEL_READ_COMPLETE, queue.poll());

        handler.removeInboundHandler();
        assertEquals(Event.HANDLER_REMOVED, queue.poll());

        // These should not be handled by the inboundHandler anymore as it was removed before
        channel.pipeline().fireChannelWritabilityChanged();
        channel.pipeline().fireUserEventTriggered(MSG);
        channel.pipeline().fireChannelRead(MSG);
        channel.pipeline().fireChannelReadComplete();

        // Should have not received any more events as it was removed before via removeInboundHandler()
        assertTrue(queue.isEmpty());
        assertTrue(channel.finish());
        assertTrue(queue.isEmpty());
    }

    @Test
    public void testOutboundEvents() {
        final Queue<Event> queue = new ArrayDeque<Event>();

        ChannelInboundHandler inboundHandler = new ChannelInboundHandlerAdapter();
        ChannelOutboundHandler outboundHandler = new ChannelOutboundHandlerAdapter() {
            @Override
            public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
                queue.add(Event.HANDLER_ADDED);
            }

            @Override
            public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
                queue.add(Event.HANDLER_REMOVED);
            }

            @Override
            public void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise)
                    throws Exception {
                queue.add(Event.BIND);
            }

            @Override
            public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress,
                                SocketAddress localAddress, ChannelPromise promise) throws Exception {
                queue.add(Event.CONNECT);
            }

            @Override
            public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
                queue.add(Event.DISCONNECT);
            }

            @Override
            public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
                queue.add(Event.CLOSE);
            }

            @Override
            public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
                queue.add(Event.DEREGISTER);
            }

            @Override
            public void read(ChannelHandlerContext ctx) throws Exception {
                queue.add(Event.READ);
            }

            @Override
            public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
                queue.add(Event.WRITE);
            }

            @Override
            public void flush(ChannelHandlerContext ctx) throws Exception {
                queue.add(Event.FLUSH);
            }
        };

        CombinedChannelDuplexHandler<ChannelInboundHandler, ChannelOutboundHandler> handler =
                new CombinedChannelDuplexHandler<ChannelInboundHandler, ChannelOutboundHandler>(
                        inboundHandler, outboundHandler);

        EmbeddedChannel channel = new EmbeddedChannel();
        channel.pipeline().addFirst(handler);

        doOutboundOperations(channel);

        assertEquals(Event.HANDLER_ADDED, queue.poll());
        assertEquals(Event.BIND, queue.poll());
        assertEquals(Event.CONNECT, queue.poll());
        assertEquals(Event.WRITE, queue.poll());
        assertEquals(Event.FLUSH, queue.poll());
        assertEquals(Event.READ, queue.poll());
        assertEquals(Event.CLOSE, queue.poll());
        assertEquals(Event.CLOSE, queue.poll());
        assertEquals(Event.DEREGISTER, queue.poll());

        handler.removeOutboundHandler();
        assertEquals(Event.HANDLER_REMOVED, queue.poll());

        // These should not be handled by the inboundHandler anymore as it was removed before
        doOutboundOperations(channel);

        // Should have not received any more events as it was removed before via removeInboundHandler()
        assertTrue(queue.isEmpty());
        assertTrue(channel.finish());
        assertTrue(queue.isEmpty());
    }

    private static void doOutboundOperations(Channel channel) {
        channel.pipeline().bind(ADDRESS);
        channel.pipeline().connect(ADDRESS);
        channel.pipeline().write(MSG);
        channel.pipeline().flush();
        channel.pipeline().read();
        channel.pipeline().disconnect();
        channel.pipeline().close();
        channel.pipeline().deregister();
    }

    @Test(timeout = 3000)
    public void testPromisesPassed() {
        ChannelOutboundHandler outboundHandler = new ChannelOutboundHandlerAdapter() {
            @Override
            public void bind(ChannelHandlerContext ctx, SocketAddress localAddress,
                             ChannelPromise promise) throws Exception {
                promise.setSuccess();
            }

            @Override
            public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress,
                                SocketAddress localAddress, ChannelPromise promise) throws Exception {
                promise.setSuccess();
            }

            @Override
            public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
                promise.setSuccess();
            }

            @Override
            public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
                promise.setSuccess();
            }

            @Override
            public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
                promise.setSuccess();
            }

            @Override
            public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
                promise.setSuccess();
            }
        };
        EmbeddedChannel ch = new EmbeddedChannel(outboundHandler,
                new CombinedChannelDuplexHandler<ChannelInboundHandler, ChannelOutboundHandler>(
                        new ChannelInboundHandlerAdapter(), new ChannelOutboundHandlerAdapter()));
        ChannelPipeline pipeline = ch.pipeline();

        ChannelPromise promise = ch.newPromise();
        pipeline.connect(new InetSocketAddress(0), null, promise);
        promise.syncUninterruptibly();

        promise = ch.newPromise();
        pipeline.bind(new InetSocketAddress(0), promise);
        promise.syncUninterruptibly();

        promise = ch.newPromise();
        pipeline.close(promise);
        promise.syncUninterruptibly();

        promise = ch.newPromise();
        pipeline.disconnect(promise);
        promise.syncUninterruptibly();

        promise = ch.newPromise();
        pipeline.write("test", promise);
        promise.syncUninterruptibly();

        promise = ch.newPromise();
        pipeline.deregister(promise);
        promise.syncUninterruptibly();
        ch.finish();
    }
}
