// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.base;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import static org.chromium.base.test.util.Matchers.fulfilledPromise;
import static org.chromium.base.test.util.Matchers.pendingPromise;
import static org.chromium.base.test.util.Matchers.rejectedPromise;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.LooperMode;
import org.robolectric.shadows.ShadowLooper;

import org.chromium.base.Promise.UnhandledRejectionException;
import org.chromium.base.test.BaseRobolectricTestRunner;

import java.util.function.Function;

/** Unit tests for {@link Promise}. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
@LooperMode(LooperMode.Mode.LEGACY)
public class PromiseTest {
    // We need a simple mutable reference type for testing.
    private static class Value {
        private int mValue;

        public int get() {
            return mValue;
        }

        public void set(int value) {
            mValue = value;
        }
    }

    /** Tests that the callback is called on fulfillment. */
    @Test
    public void callback() {
        final Value value = new Value();

        Promise<Integer> promise = new Promise<>();
        promise.then(PromiseTest.setValue(value, 1));

        assertEquals(0, value.get());

        promise.fulfill(1);
        assertEquals(1, value.get());
    }

    /** Tests that multiple callbacks are called. */
    @Test
    public void multipleCallbacks() {
        final Value value = new Value();

        Promise<Integer> promise = new Promise<>();
        Callback<Integer> callback =
                unusedArg -> {
                    value.set(value.get() + 1);
                };
        promise.then(callback);
        promise.then(callback);

        assertEquals(0, value.get());

        promise.fulfill(0);
        assertEquals(2, value.get());
    }

    /** Tests that a callback is called immediately when given to a fulfilled Promise. */
    @Test
    public void callbackOnFulfilled() {
        final Value value = new Value();

        Promise<Integer> promise = Promise.fulfilled(0);
        assertEquals(0, value.get());

        promise.then(PromiseTest.setValue(value, 1));

        assertEquals(1, value.get());
    }

    /** Tests that promises can chain synchronous functions correctly. */
    @Test
    public void promiseChaining() {
        Promise<Integer> promise = new Promise<>();
        final Value value = new Value();

        promise.then((Integer arg) -> arg.toString())
                .then((String arg) -> arg + arg)
                .then(
                        result -> {
                            value.set(result.length());
                        });

        promise.fulfill(123);
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        assertEquals(6, value.get());
    }

    /** Tests that promises can chain asynchronous functions correctly. */
    @Test
    public void promiseChainingAsyncFunctions() {
        Promise<Integer> promise = new Promise<>();
        final Value value = new Value();

        final Promise<String> innerPromise = new Promise<>();

        promise.then(arg -> innerPromise)
                .then(
                        result -> {
                            value.set(result.length());
                        });

        assertEquals(0, value.get());

        promise.fulfill(5);
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        assertEquals(0, value.get());

        innerPromise.fulfill("abc");
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        assertEquals(3, value.get());
    }

    /** Tests that a Promise that does not use its result does not throw on rejection. */
    @Test
    public void rejectPromiseNoCallbacks() {
        Promise<Integer> promise = new Promise<>();

        boolean caught = false;
        try {
            promise.reject();
            ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        } catch (UnhandledRejectionException e) {
            caught = true;
        }
        assertFalse(caught);
    }

    /** Tests that a Promise that uses its result throws on rejection if it has no handler. */
    @Test
    public void rejectPromiseNoHandler() {
        Promise<Integer> promise = new Promise<>();
        promise.then(PromiseTest.identity()).then(PromiseTest.pass());

        boolean caught = false;
        try {
            promise.reject();
            ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        } catch (UnhandledRejectionException e) {
            caught = true;
        }
        assertTrue(caught);
    }

    /** Tests that a Promise that handles rejection does not throw on rejection. */
    @Test
    public void rejectPromiseHandled() {
        Promise<Integer> promise = new Promise<>();
        promise.then(PromiseTest.identity()).then(PromiseTest.pass(), PromiseTest.pass());

        boolean caught = false;
        try {
            promise.reject();
            ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        } catch (UnhandledRejectionException e) {
            caught = true;
        }
        assertFalse(caught);
    }

    /** Tests that rejections carry the exception information. */
    @Test
    public void rejectionInformation() {
        Promise<Integer> promise = new Promise<>();
        promise.then(PromiseTest.pass());

        String message = "Promise Test";
        try {
            promise.reject(new NegativeArraySizeException(message));
            ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
            fail();
        } catch (UnhandledRejectionException e) {
            assertTrue(e.getCause() instanceof NegativeArraySizeException);
            assertEquals(e.getCause().getMessage(), message);
        }
    }

    /** Tests that rejections propagate. */
    @Test
    public void rejectionChaining() {
        final Value value = new Value();
        Promise<Integer> promise = new Promise<>();

        Promise<Integer> result = promise.then(PromiseTest.identity()).then(PromiseTest.identity());

        result.then(PromiseTest.pass(), PromiseTest.setValue(value, 5));

        promise.reject(new Exception());
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();

        assertEquals(5, value.get());
        assertTrue(result.isRejected());
    }

    /** Tests that Promises get rejected if a Function throws. */
    @Test
    public void rejectOnThrow() {
        Value value = new Value();
        Promise<Integer> promise = new Promise<>();
        promise.then(
                        (Function)
                                unusedArg -> {
                                    throw new IllegalArgumentException();
                                })
                .then(PromiseTest.pass(), PromiseTest.setValue(value, 5));

        promise.fulfill(0);

        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        assertEquals(5, value.get());
    }

    /** Tests that Promises get rejected if an AsyncFunction throws. */
    @Test
    public void rejectOnAsyncThrow() {
        Value value = new Value();
        Promise<Integer> promise = new Promise<>();

        promise.then(
                        (Promise.AsyncFunction)
                                unusedArg -> {
                                    throw new IllegalArgumentException();
                                })
                .then(PromiseTest.pass(), PromiseTest.setValue(value, 5));

        promise.fulfill(0);

        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        assertEquals(5, value.get());
    }

    /** Tests that Promises get rejected if an AsyncFunction rejects. */
    @Test
    public void rejectOnAsyncReject() {
        Value value = new Value();
        Promise<Integer> promise = new Promise<>();
        final Promise<Integer> inner = new Promise<>();

        promise.then(unusedArg -> inner).then(PromiseTest.pass(), PromiseTest.setValue(value, 5));

        promise.fulfill(0);

        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        assertEquals(0, value.get());

        inner.reject();

        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        assertEquals(5, value.get());
    }

    @Test
    public void andFinallyOnFulfill() {
        Value value = new Value();
        Promise<Integer> promise = new Promise<>();

        promise.andFinally(() -> value.set(5));
        assertEquals(0, value.get());

        promise.fulfill(0);

        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        assertEquals(5, value.get());
    }

    @Test
    public void andFinallyOnReject() {
        Value value = new Value();
        Promise<Integer> promise = new Promise<>();

        promise.andFinally(() -> value.set(5));
        assertEquals(0, value.get());

        promise.reject();

        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        assertEquals(5, value.get());
    }

    @Test
    public void andFinallyChainingFulfillment() {
        Value value = new Value();
        Promise<Integer> promise = new Promise<>();

        Promise<Integer> chainedPromise =
                promise.andFinally(() -> value.set(value.get() + 1))
                        .then(Object::toString)
                        .andFinally(() -> value.set(value.get() * 10))
                        .then(String::length);
        assertEquals(0, value.get());
        assertThat(chainedPromise, is(pendingPromise()));

        promise.fulfill(123);
        assertThat(chainedPromise, is(fulfilledPromise()));
        assertEquals(10, value.get());
        assertEquals(3, chainedPromise.getResult().intValue());
    }

    @Test
    public void andFinallyChainingRejection() {
        Value value = new Value();
        Promise<Integer> promise = new Promise<>();

        Promise<Integer> chainedPromise =
                promise.andFinally(() -> value.set(value.get() + 1))
                        .then(Object::toString)
                        .andFinally(() -> value.set(value.get() * 10))
                        .then(String::length);
        assertEquals(0, value.get());
        assertThat(chainedPromise, is(pendingPromise()));

        promise.reject();
        assertEquals(10, value.get()); // Both `andFinally()` still run.
        assertThat(chainedPromise, is(rejectedPromise()));
    }

    /** Convenience method that returns a Callback that does nothing with its result. */
    private static <T> Callback<T> pass() {
        return unusedArg -> {};
    }

    /** Convenience method that returns a Function that just passes through its argument. */
    private static <T> Function<T, T> identity() {
        return argument -> argument;
    }

    /** Convenience method that returns a Callback that sets the given Value on execution. */
    private static <T> Callback<T> setValue(final Value toSet, final int value) {
        return unusedArg -> {
            toSet.set(value);
        };
    }
}
