/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF 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 org.apache.ignite.internal.util.future;

import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import javax.cache.CacheException;
import org.apache.ignite.IgniteCheckedException;
import org.apache.ignite.IgniteException;
import org.apache.ignite.internal.IgniteInternalFuture;
import org.apache.ignite.internal.util.typedef.C1;
import org.apache.ignite.internal.util.typedef.CI1;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.apache.ignite.lang.IgniteFuture;
import org.apache.ignite.lang.IgniteInClosure;
import org.apache.ignite.testframework.GridTestUtils;
import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
import org.jetbrains.annotations.NotNull;
import org.junit.Test;

/**
 *
 */
public class IgniteFutureImplTest extends GridCommonAbstractTest {
    /** Context thread name. */
    private static final String CTX_THREAD_NAME = "test-async";

    /** Custom thread name. */
    private static final String CUSTOM_THREAD_NAME = "test-custom-async";

    /** Test executor. */
    private ExecutorService ctxExec;

    /** Custom executor. */
    private ExecutorService customExec;

    /** {@inheritDoc} */
    @Override protected void beforeTest() throws Exception {
        ctxExec = createExecutor(CTX_THREAD_NAME);
        customExec = createExecutor(CUSTOM_THREAD_NAME);
    }

    /** {@inheritDoc} */
    @Override protected void afterTest() throws Exception {
        U.shutdownNow(getClass(), ctxExec, log);
        U.shutdownNow(getClass(), customExec, log);

        ctxExec = null;
        customExec = null;
    }

    /**
     * @throws Exception If failed.
     */
    @Test
    public void testFutureGet() throws Exception {
        GridFutureAdapter<String> fut0 = new GridFutureAdapter<>();

        IgniteFutureImpl<String> fut = createFuture(fut0);

        assertFalse(fut.isDone());

        U.sleep(100);

        fut0.onDone("test");

        assertEquals("test", fut.get());

        assertTrue(fut.isDone());
    }

    /**
     * @throws Exception If failed.
     */
    @Test
    public void testFutureException() throws Exception {
        GridFutureAdapter<String> fut0 = new GridFutureAdapter<>();

        final IgniteFutureImpl<String> fut = createFuture(fut0);

        assertFalse(fut.isDone());

        U.sleep(100);

        IgniteCheckedException err0 = new IgniteCheckedException("test error");

        fut0.onDone(err0);

        Exception err = (Exception)GridTestUtils.assertThrows(log, new Callable<Void>() {
            @Override public Void call() throws Exception {
                fut.get();

                return null;
            }
        }, expectedException(), "test error");

        assertExpectedException(err, err0);

        assertTrue(fut.isDone());

        err = (Exception)GridTestUtils.assertThrows(log, new Callable<Void>() {
            @Override public Void call() throws Exception {
                fut.get();

                return null;
            }
        }, expectedException(), null);

        assertExpectedException(err, err0);
    }

    /**
     * @throws Exception If failed.
     */
    @Test
    public void testFutureIgniteException() throws Exception {
        GridFutureAdapter<String> fut0 = new GridFutureAdapter<>();

        final IgniteFutureImpl<String> fut = createFuture(fut0);

        IgniteException err0 = new IgniteException("test error");

        fut0.onDone(err0);

        Exception err = (Exception)GridTestUtils.assertThrows(log, new Callable<Void>() {
            @Override public Void call() throws Exception {
                fut.get();

                return null;
            }
        }, expectedException(), "test error");

        assertExpectedException(err, err0);
    }

    /**
     * @throws Exception If failed.
     */
    @Test
    public void testListeners() throws Exception {
        GridFutureAdapter<String> fut0 = new GridFutureAdapter<>();

        IgniteFutureImpl<String> fut = createFuture(fut0);

        final AtomicInteger lsnr1Cnt = new AtomicInteger();

        IgniteInClosure<? super IgniteFuture<String>> lsnr1 = new CI1<IgniteFuture<String>>() {
            @Override public void apply(IgniteFuture<String> fut) {
                assertEquals("test", fut.get());

                lsnr1Cnt.incrementAndGet();
            }
        };

        final AtomicInteger lsnr2Cnt = new AtomicInteger();

        IgniteInClosure<? super IgniteFuture<String>> lsnr2 = new CI1<IgniteFuture<String>>() {
            @Override public void apply(IgniteFuture<String> fut) {
                assertEquals("test", fut.get());

                lsnr2Cnt.incrementAndGet();
            }
        };

        assertFalse(fut.isDone());

        fut.listen(lsnr1);
        fut.listen(lsnr2);

        U.sleep(100);

        assertEquals(0, lsnr1Cnt.get());
        assertEquals(0, lsnr2Cnt.get());

        fut0.onDone("test");

        assertEquals(1, lsnr1Cnt.get());
        assertEquals(1, lsnr2Cnt.get());
    }

    /**
     * @throws Exception If failed.
     */
    @Test
    public void testListenersOnError() throws Exception {
        {
            GridFutureAdapter<String> fut0 = new GridFutureAdapter<>();

            IgniteFutureImpl<String> fut = createFuture(fut0);

            final IgniteException err0 = new IgniteException("test error");

            final AtomicBoolean passed = new AtomicBoolean();

            IgniteInClosure<? super IgniteFuture<String>> lsnr1 = new CI1<IgniteFuture<String>>() {
                @Override public void apply(IgniteFuture<String> fut) {
                    try {
                        fut.get();

                        fail();
                    }
                    catch (IgniteException | CacheException err) {
                        assertExpectedException(err, err0);

                        passed.set(true);
                    }
                }
            };

            fut.listen(lsnr1);

            fut0.onDone(err0);

            assertTrue(passed.get());
        }

        {
            GridFutureAdapter<String> fut0 = new GridFutureAdapter<>();

            IgniteFutureImpl<String> fut = createFuture(fut0);

            final IgniteCheckedException err0 = new IgniteCheckedException("test error");

            final AtomicBoolean passed = new AtomicBoolean();

            IgniteInClosure<? super IgniteFuture<String>> lsnr1 = new CI1<IgniteFuture<String>>() {
                @Override public void apply(IgniteFuture<String> fut) {
                    try {
                        fut.get();

                        fail();
                    }
                    catch (IgniteException | CacheException err) {
                        assertExpectedException(err, err0);

                        passed.set(true);
                    }
                }
            };

            fut.listen(lsnr1);

            fut0.onDone(err0);

            assertTrue(passed.get());
        }
    }

    /**
     * @throws Exception If failed.
     */
    @Test
    public void testAsyncListeners() throws Exception {
        GridFutureAdapter<String> fut0 = new GridFutureAdapter<>();

        IgniteFutureImpl<String> fut = createFuture(fut0);

        final CountDownLatch latch1 = new CountDownLatch(1);

        IgniteInClosure<? super IgniteFuture<String>> lsnr1 = createAsyncListener(latch1, CUSTOM_THREAD_NAME, null);

        assertFalse(fut.isDone());

        fut.listenAsync(lsnr1, customExec);

        U.sleep(100);

        assertEquals(1, latch1.getCount());

        fut0.onDone("test");

        assert latch1.await(1, TimeUnit.SECONDS) : latch1.getCount();

        checkAsyncListener(fut);
        checkAsyncListener(createFuture(new GridFinishedFuture<>("test")));
    }

    /**
     * @param fut Future.
     */
    private void checkAsyncListener(IgniteFutureImpl<String> fut) throws InterruptedException {
        final CountDownLatch latch = new CountDownLatch(1);

        IgniteInClosure<? super IgniteFuture<String>> lsnr = createAsyncListener(latch, CUSTOM_THREAD_NAME, null);

        fut.listenAsync(lsnr, customExec);

        assert latch.await(1, TimeUnit.SECONDS) : latch.getCount();
    }

    /**
     * @throws Exception If failed.
     */
    @Test
    public void testAsyncListenersOnError() throws Exception {
        checkAsyncListenerOnError(new IgniteException("Test exception"));
        checkAsyncListenerOnError(new IgniteCheckedException("Test checked exception"));
    }

    /**
     * @param err0 Test exception.
     */
    private void checkAsyncListenerOnError(Exception err0) throws InterruptedException {
        GridFutureAdapter<String> fut0 = new GridFutureAdapter<>();

        IgniteFutureImpl<String> fut = createFuture(fut0);

        final CountDownLatch latch1 = new CountDownLatch(1);

        IgniteInClosure<? super IgniteFuture<String>> lsnr1 = createAsyncListener(latch1, CUSTOM_THREAD_NAME, err0);

        fut.listenAsync(lsnr1, customExec);

        assertEquals(1, latch1.getCount());

        fut0.onDone(err0);

        assert latch1.await(1, TimeUnit.SECONDS);

        checkAsyncListenerOnError(err0, fut);
        checkAsyncListenerOnError(err0, createFuture(new GridFinishedFuture<String>(err0)));
    }

    /**
     * @param err0 Err 0.
     * @param fut Future.
     */
    private void checkAsyncListenerOnError(Exception err0, IgniteFutureImpl<String> fut) throws InterruptedException {
        final CountDownLatch latch = new CountDownLatch(1);

        IgniteInClosure<? super IgniteFuture<String>> lsnr = createAsyncListener(latch, CUSTOM_THREAD_NAME, err0);

        fut.listenAsync(lsnr, customExec);

        assert latch.await(1, TimeUnit.SECONDS);
    }

    /**
     * @param latch Latch.
     */
    @NotNull private CI1<IgniteFuture<String>> createAsyncListener(
        final CountDownLatch latch,
        final String threadName,
        final Exception err
    ) {
        return new CI1<IgniteFuture<String>>() {
            @Override public void apply(IgniteFuture<String> fut) {
                try {
                    String tname = Thread.currentThread().getName();

                    assert tname.contains(threadName) : tname;

                    assertEquals("test", fut.get());

                    if (err != null)
                        fail();
                }
                catch (IgniteException | CacheException e) {
                    if (err != null)
                        assertExpectedException(e, err);
                    else
                        throw e;
                }
                finally {
                    latch.countDown();
                }
            }
        };
    }

    /**
     * @throws Exception If failed.
     */
    @Test
    public void testChain() throws Exception {
        GridFutureAdapter<String> fut0 = new GridFutureAdapter<>();

        IgniteFutureImpl<String> fut = createFuture(fut0);

        IgniteFuture<Integer> chained = fut.chain(new C1<IgniteFuture<String>, Integer>() {
            @Override public Integer apply(IgniteFuture<String> fut) {
                return Integer.valueOf(fut.get());
            }
        });

        assertFalse(chained.isDone());

        U.sleep(100);

        final AtomicInteger lsnrCnt = new AtomicInteger();

        chained.listen(new CI1<IgniteFuture<Integer>>() {
            @Override public void apply(IgniteFuture<Integer> fut) {
                assertEquals(10, (int)fut.get());

                lsnrCnt.incrementAndGet();
            }
        });

        fut0.onDone("10");

        assertTrue(chained.isDone());

        assertEquals(10, (int)chained.get());

        assertEquals(1, lsnrCnt.get());

        assertTrue(fut.isDone());

        assertEquals("10", fut.get());
    }

    /**
     * @throws Exception If failed.
     */
    @Test
    public void testChainError() throws Exception {
        {
            GridFutureAdapter<String> fut0 = new GridFutureAdapter<>();

            IgniteFutureImpl<String> fut = createFuture(fut0);

            final IgniteException err0 = new IgniteException("test error");

            final AtomicBoolean chainedPassed = new AtomicBoolean();

            IgniteFuture<Integer> chained = fut.chain(new C1<IgniteFuture<String>, Integer>() {
                @Override public Integer apply(IgniteFuture<String> fut) {
                    try {
                        fut.get();

                        fail();

                        return -1;
                    }
                    catch (IgniteException | CacheException err) {
                        assertExpectedException(err, err0);

                        chainedPassed.set(true);

                        throw err;
                    }
                }
            });

            final AtomicBoolean lsnrPassed = new AtomicBoolean();

            IgniteInClosure<? super IgniteFuture<Integer>> lsnr1 = new CI1<IgniteFuture<Integer>>() {
                @Override public void apply(IgniteFuture<Integer> fut) {
                    try {
                        fut.get();

                        fail();
                    }
                    catch (IgniteException | CacheException err) {
                        assertExpectedException(err, err0);

                        lsnrPassed.set(true);
                    }
                }
            };

            chained.listen(lsnr1);

            fut0.onDone(err0);

            assertTrue(chainedPassed.get());

            assertTrue(lsnrPassed.get());

            try {
                chained.get();

                fail();
            }
            catch (IgniteException | CacheException err) {
                assertExpectedException(err, err0);
            }

            try {
                fut.get();

                fail();
            }
            catch (IgniteException | CacheException err) {
                assertExpectedException(err, err0);
            }
        }

        {
            GridFutureAdapter<String> fut0 = new GridFutureAdapter<>();

            IgniteFutureImpl<String> fut = createFuture(fut0);

            final IgniteCheckedException err0 = new IgniteCheckedException("test error");

            final AtomicBoolean chainedPassed = new AtomicBoolean();

            IgniteFuture<Integer> chained = fut.chain(new C1<IgniteFuture<String>, Integer>() {
                @Override public Integer apply(IgniteFuture<String> fut) {
                    try {
                        fut.get();

                        fail();

                        return -1;
                    }
                    catch (IgniteException | CacheException err) {
                        assertExpectedException(err, err0);

                        chainedPassed.set(true);

                        throw err;
                    }
                }
            });

            final AtomicBoolean lsnrPassed = new AtomicBoolean();

            IgniteInClosure<? super IgniteFuture<Integer>> lsnr1 = new CI1<IgniteFuture<Integer>>() {
                @Override public void apply(IgniteFuture<Integer> fut) {
                    try {
                        fut.get();

                        fail();
                    }
                    catch (IgniteException | CacheException err) {
                        assertExpectedException(err, err0);

                        lsnrPassed.set(true);
                    }
                }
            };

            chained.listen(lsnr1);

            fut0.onDone(err0);

            assertTrue(chainedPassed.get());

            assertTrue(lsnrPassed.get());

            try {
                chained.get();

                fail();
            }
            catch (IgniteException | CacheException err) {
                assertExpectedException(err, err0);
            }

            try {
                fut.get();

                fail();
            }
            catch (IgniteException | CacheException err) {
                assertExpectedException(err, err0);
            }
        }
    }

    /**
     * @throws Exception If failed.
     */
    @Test
    public void testChainAsync() throws Exception {
        GridFutureAdapter<String> fut0 = new GridFutureAdapter<>();

        IgniteFuture<String> fut = createFuture(fut0);

        C1<IgniteFuture<String>, Integer> chainClos = new C1<IgniteFuture<String>, Integer>() {
            @Override public Integer apply(IgniteFuture<String> fut) {
                assertEquals(CUSTOM_THREAD_NAME, Thread.currentThread().getName());

                return Integer.valueOf(fut.get());
            }
        };

        IgniteFuture<Integer> chained1 = fut.chainAsync(chainClos, customExec);

        assertFalse(chained1.isDone());

        final CountDownLatch latch = new CountDownLatch(1);

        /** */
        class TestClosure implements CI1<IgniteFuture<Integer>> {
            private final CountDownLatch latch;

            /** */
            private TestClosure(CountDownLatch latch) {
                this.latch = latch;
            }

            /** {@inheritDoc} */
            @Override public void apply(IgniteFuture<Integer> fut) {
                assertEquals(CUSTOM_THREAD_NAME, Thread.currentThread().getName());
                assertEquals(10, (int)fut.get());

                latch.countDown();
            }
        }

        chained1.listen(new TestClosure(latch));

        fut0.onDone("10");

        // Chained future will be completed asynchronously.
        chained1.get(100, TimeUnit.MILLISECONDS);

        assertTrue(chained1.isDone());

        assertEquals(10, (int)chained1.get());

        assert latch.await(100, TimeUnit.MILLISECONDS);

        assertTrue(fut.isDone());

        assertEquals("10", fut.get());

        // Test finished future
        GridFinishedFuture<String> ffut0 = new GridFinishedFuture<>("10");

        CountDownLatch latch1 = new CountDownLatch(1);

        IgniteFuture<Integer> chained2 = createFuture(ffut0).chainAsync(chainClos, customExec);

        chained2.listen(new TestClosure(latch1));

        chained2.get(100, TimeUnit.MILLISECONDS);

        assertTrue(chained2.isDone());

        assertEquals(10, (int)chained2.get());

        assert latch1.await(100, TimeUnit.MILLISECONDS);
    }

    /**
     * @throws Exception If failed.
     */
    @Test
    public void testChainAsyncOnError() throws Exception {
        checkChainedOnError(new IgniteException("Test exception"));
        checkChainedOnError(new IgniteCheckedException("Test checked exception"));
        checkChainedOnErrorFinishedFuture(new IgniteException("Test exception"));
        checkChainedOnErrorFinishedFuture(new IgniteCheckedException("Test checked exception"));
    }

    /**
     * @param err Exception.
     * @throws Exception If failed.
     */
    private void checkChainedOnError(final Exception err) throws Exception {
        GridFutureAdapter<String> fut0 = new GridFutureAdapter<>();

        IgniteFutureImpl<String> fut = createFuture(fut0);

        // Chain callback will be invoked in specific executor.
        IgniteFuture<Integer> chained1 = fut.chainAsync(new C1<IgniteFuture<String>, Integer>() {
            @Override public Integer apply(IgniteFuture<String> fut) {
                assertEquals(CUSTOM_THREAD_NAME, Thread.currentThread().getName());

                try {
                    fut.get();

                    fail();
                }
                catch (IgniteException | CacheException e) {
                    assertExpectedException(e, err);

                    throw e;
                }

                return -1;
            }
        }, customExec);

        assertFalse(chained1.isDone());
        assertFalse(fut.isDone());

        final CountDownLatch latch = new CountDownLatch(1);

        chained1.listen(new CI1<IgniteFuture<Integer>>() {
            @Override public void apply(IgniteFuture<Integer> fut) {
                try {
                    assertEquals(CUSTOM_THREAD_NAME, Thread.currentThread().getName());

                    fut.get();

                    fail();
                }
                catch (IgniteException | CacheException e) {
                    assertExpectedException(e, err);
                }
                finally {
                    latch.countDown();
                }
            }
        });

        fut0.onDone(err);

        assertExceptionThrown(err, chained1);
        assertExceptionThrown(err, fut);

        assertTrue(chained1.isDone());
        assertTrue(fut.isDone());

        assert latch.await(100, TimeUnit.MILLISECONDS);
    }

    /**
     * @param err Err.
     */
    private void checkChainedOnErrorFinishedFuture(final Exception err) throws Exception {
        IgniteFutureImpl<String> fut = createFuture(new GridFinishedFuture<String>(err));

        // Chain callback will be invoked in specific executor.
        IgniteFuture<Integer> chained1 = fut.chainAsync(new C1<IgniteFuture<String>, Integer>() {
            @Override public Integer apply(IgniteFuture<String> fut) {
                assertEquals(CUSTOM_THREAD_NAME, Thread.currentThread().getName());

                try {
                    fut.get();

                    fail();
                }
                catch (IgniteException e) {
                    assertExpectedException(e, err);

                    throw e;
                }

                return -1;
            }
        }, customExec);

        assertExceptionThrown(err, chained1);
        assertExceptionThrown(err, fut);

        assertTrue(chained1.isDone());
        assertTrue(fut.isDone());
    }

    /**
     * @param err Expected exception.
     * @param fut Future.
     */
    private void assertExceptionThrown(Exception err, IgniteFuture<?> fut) {
        try {
            fut.get();

            fail();
        }
        catch (IgniteException | CacheException e) {
            assertExpectedException(e, err);
        }
    }

    /**
     * @param e Actual exception.
     * @param exp Expected exception.
     */
    protected void assertExpectedException(Exception e, Exception exp) {
        if (exp instanceof IgniteException)
            assertEquals(exp, e);
        else
            assertEquals(exp, e.getCause());
    }

    /**
     * @param name Name.
     */
    @NotNull private ExecutorService createExecutor(final String name) {
        return Executors.newSingleThreadExecutor(new ThreadFactory() {
            @Override public Thread newThread(@NotNull Runnable r) {
                Thread t = new Thread(r);

                t.setName(name);

                return t;
            }
        });
    }

    /**
     * @param fut Future.
     */
    protected <V> IgniteFutureImpl<V> createFuture(IgniteInternalFuture<V> fut) {
        return new IgniteFutureImpl<>(fut);
    }

    /**
     *
     */
    protected Class<? extends Exception> expectedException() {
        return IgniteException.class;
    }
}
