// Copyright 2016 The Chromium Authors. All rights reserved.
// 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.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

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

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

/** Unit tests for {@link Promise}. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
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(value.get(), 0);

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

    /** 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(value.get(), 0);

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

    /** 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(value.get(), 0);

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

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

    /** 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(value.get(), 5);
        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(value.get(), 5);
    }

    /** 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(value.get(), 5);
    }

    /** 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(value.get(), 0);

        inner.reject();

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

    /** 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);
        };
    }
}
