/*
 * Copyright 2013 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.util.concurrent;

import org.junit.Test;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;

@SuppressWarnings("unchecked")
public class DefaultPromiseTest {

    @Test
    public void testNoStackOverflowErrorWithImmediateEventExecutorA() throws Exception {
        final Promise<Void>[] p = new DefaultPromise[128];
        for (int i = 0; i < p.length; i ++) {
            final int finalI = i;
            p[i] = new DefaultPromise<Void>(ImmediateEventExecutor.INSTANCE);
            p[i].addListener(new FutureListener<Void>() {
                @Override
                public void operationComplete(Future<Void> future) throws Exception {
                    if (finalI + 1 < p.length) {
                        p[finalI + 1].setSuccess(null);
                    }
                }
            });
        }

        p[0].setSuccess(null);

        for (Promise<Void> a: p) {
            assertThat(a.isSuccess(), is(true));
        }
    }

    @Test
    public void testNoStackOverflowErrorWithImmediateEventExecutorB() throws Exception {
        final Promise<Void>[] p = new DefaultPromise[128];
        for (int i = 0; i < p.length; i ++) {
            final int finalI = i;
            p[i] = new DefaultPromise<Void>(ImmediateEventExecutor.INSTANCE);
            p[i].addListener(new FutureListener<Void>() {
                @Override
                public void operationComplete(Future<Void> future) throws Exception {
                    DefaultPromise.notifyListener(ImmediateEventExecutor.INSTANCE, future, new FutureListener<Void>() {
                        @Override
                        public void operationComplete(Future<Void> future) throws Exception {
                            if (finalI + 1 < p.length) {
                                p[finalI + 1].setSuccess(null);
                            }
                        }
                    });
                }
            });
        }

        p[0].setSuccess(null);

        for (Promise<Void> a: p) {
            assertThat(a.isSuccess(), is(true));
        }
    }

    @Test
    public void testListenerNotifyOrder() throws Exception {
        EventExecutor executor = new TestEventExecutor();
        final BlockingQueue<FutureListener<Void>> listeners = new LinkedBlockingQueue<FutureListener<Void>>();
        int runs = 100000;

        for (int i = 0; i < runs; i++) {
            final Promise<Void> promise = new DefaultPromise<Void>(executor);
            final FutureListener<Void> listener1 = new FutureListener<Void>() {
                @Override
                public void operationComplete(Future<Void> future) throws Exception {
                    listeners.add(this);
                }
            };
            final FutureListener<Void> listener2 = new FutureListener<Void>() {
                @Override
                public void operationComplete(Future<Void> future) throws Exception {
                    listeners.add(this);
                }
            };
            final FutureListener<Void> listener4 = new FutureListener<Void>() {
                @Override
                public void operationComplete(Future<Void> future) throws Exception {
                    listeners.add(this);
                }
            };
            final FutureListener<Void> listener3 = new FutureListener<Void>() {
                @Override
                public void operationComplete(Future<Void> future) throws Exception {
                    // Ensure listener4 is notified *after* this method returns to maintain the order.
                    future.addListener(listener4);
                    listeners.add(this);
                }
            };

            GlobalEventExecutor.INSTANCE.execute(new Runnable() {
                @Override
                public void run() {
                    promise.setSuccess(null);
                }
            });

            promise.addListener(listener1).addListener(listener2).addListener(listener3);

            assertSame("Fail during run " + i + " / " + runs, listener1, listeners.take());
            assertSame("Fail during run " + i + " / " + runs, listener2, listeners.take());
            assertSame("Fail during run " + i + " / " + runs, listener3, listeners.take());
            assertSame("Fail during run " + i + " / " + runs, listener4, listeners.take());
            assertTrue("Fail during run " + i + " / " + runs, listeners.isEmpty());
        }
        executor.shutdownGracefully().sync();
    }

    @Test
    public void testListenerNotifyLater() throws Exception {
        // Testing first execution path in DefaultPromise
        testListenerNotifyLater(1);

        // Testing second execution path in DefaultPromise
        testListenerNotifyLater(2);
    }

    private static void testListenerNotifyLater(final int numListenersBefore) throws Exception {
        EventExecutor executor = new TestEventExecutor();
        int expectedCount = numListenersBefore + 2;
        final CountDownLatch latch = new CountDownLatch(expectedCount);
        final FutureListener<Void> listener = new FutureListener<Void>() {
            @Override
            public void operationComplete(Future<Void> future) throws Exception {
                latch.countDown();
            }
        };
        final Promise<Void> promise = new DefaultPromise<Void>(executor);
        executor.execute(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < numListenersBefore; i++) {
                    promise.addListener(listener);
                }
                promise.setSuccess(null);

                GlobalEventExecutor.INSTANCE.execute(new Runnable() {
                    @Override
                    public void run() {
                        promise.addListener(listener);
                    }
                });
                promise.addListener(listener);
            }
        });

        assertTrue("Should have notifed " + expectedCount + " listeners", latch.await(5, TimeUnit.SECONDS));
        executor.shutdownGracefully().sync();
    }

    private static final class TestEventExecutor extends SingleThreadEventExecutor {
        TestEventExecutor() {
            super(null, new DefaultExecutorServiceFactory(TestEventExecutor.class).newExecutorService(1), true);
        }

        @Override
        protected void run() {
            Runnable task = takeTask();
            if (task != null) {
                task.run();
                updateLastExecutionTime();
            }

            if (confirmShutdown()) {
                cleanupAndTerminate(true);
            } else {
                scheduleExecution();
            }
        }
    }
}
