/* Test the Async module. */

if (!('assertEquals' in this)) { /* allow running this test standalone */
    imports.lang.copyPublicProperties(imports.jsUnit, this);
    gjstestRun = function() { return imports.jsUnit.gjstestRun(window); };
}

const Async = imports.async;
const Lang = imports.lang;
const Mainloop = imports.mainloop;

/* Simple synchronous function. */
let example = function() {
    /* definitions */
    let getX = function() { return 2; };
    let getY = function() { return 3; };
    let add = function(x, y) { return x+y; };

    /* computation */
    let x = getX();
    let y = getY();
    let z = add(x, y);
    if (z<0) throw new Error("Negative!");
    return z;
};

/* Asynchronous equivalent, using a generator. */
let exampleAsync = Async.fromGenerator(function() {
    /* definitions */
    let getX = Async.fromSync(function() { return 2; });
    let getY = Async.fromSync(function() { return 3; });
    let add = Async.fromSync(function(x, y) { return x+y; });

    /* computation */
    let x = yield getX.asyncCall(this);
    let y = yield getY.asyncCall(this);
    let z = yield add.asyncCall(this, x, y);
    if (z<0) throw new Error("Negative!");
    Async.asyncReturn(z);
});

function testExample() {
    /* synchronous function */
    assertEquals(5, example());
    /* Asynchronous Function. (invoked synchronously) */
    exampleAsync(function(r) {
                     assertEquals(5, r);
                 }, function() {
                     assertTrue(false);
                 });
}

/* OK, now use Mainloop to actually invoke all the parts asynchronously. */
let exampleAsync2 = Async.fromGenerator(function() {
    /* definitions */
    let getInIdle = Async.asyncFunc(function(onReturn, onError, x) {
        try {
            Mainloop.idle_add(function() { onReturn(x); return false; });
        } catch (e) {
            onError(e);
        }
    });
    let add = Async.fromSync(function(x, y) { return x+y; });

    /* computation */
    let x = yield getInIdle.asyncCall(this, 2);
    let y = yield getInIdle.asyncCall(this, 3);
    let z = yield getInIdle.asyncCall(this, /* yield can be an expression: */
                                      (yield add.asyncCall(this, x, y)));

    if (z<0) throw new Error("Negative!");
    Async.asyncReturn(z);
});

function testExampleAsync() {
    /* Asynchronous Function. (invoked asynchronously) */
    let success = false;
    exampleAsync2(function(r) {
                     success = (r==5);
                     Mainloop.quit('asyncTest');
                 }, function() {
                     success = false;
                     Mainloop.quit('asyncTest');
                 });
    Mainloop.run('asyncTest');
    assertTrue(success);
}

/** Cached getter functionality. */
function testCachedGet() {
    let i = 0;
    let f = Async.fromSync(function() { return ++i; });
    let promise = f.asyncCall(this);

    /* We only increment i once, no matter how many times we get the
     * value of the promise. */
    promise.get(function() {}, function() {});
    assertEquals(1, i);
    promise.get(function() {}, function() {});
    assertEquals(1, i);
    promise.get(function() {}, function() {});
    assertEquals(1, i);

    /* works even when the function throws an exception */
    f = Async.fromSync(function() { ++i; throw new Error("whoa"); });
    promise = f.asyncCall(this);
    promise.get(function(){}, function(){});
    assertEquals(2, i);
    promise.get(function(){}, function(){});
    assertEquals(2, i);
}

/* Test exception handling in the generator transformation. */
function testExGen() {
    let add = Async.asyncFunc(function (onReturn, onError, x, y) {
        onReturn(x+y);
    });
    let thrower = Async.asyncFunc(function(onReturn, onError) {
        onError(new Error("this is an error"));
    });

    let cleanedUp = false;
    let foo = Async.fromGenerator(function foo() {
        let y = yield add.asyncCall(this, 2, 3);
        try {
            let z = yield thrower.asyncCall(this);
            return;
        } catch (e) {
            y = yield add.asyncCall(this, y, 1);
            Async.asyncReturn(y);
        } finally {
            cleanedUp = true;
        }
    });

    let success = false;
    foo(function(v) { success=(v==6); }, function(e) { success=false; });
    assertTrue(success);
    assertTrue(cleanedUp);
}

/* Demonstrate how to make an asyncCall, preserving 'this' */
function AFoo() {
    this._init();
}
AFoo.prototype = {
    _init : function() {
        this.foo = 'bar';
    },
    getFoo : Async.asyncFunc(function(onReturn, onError, x) {
        onReturn(this.foo + (x||''));
    }),
    testMe : Async.fromGenerator(function(x) {
        let y = yield this.getFoo.asyncCall(this, x);
        Async.asyncReturn(y);
    }),
    testEnd : Async.argsAtEnd(1, Async.fromGenerator(function(x) {
        let y = yield this.getFoo.asyncCall(this, x);
        Async.asyncReturn(y);
    }))
};
function testAFoo() {
    let a = new AFoo();
    assertTrue(a.foo == 'bar');

    // several different ways to ensure a.getFoo has this bound to 'a':
    let task1 = Async.asyncCall(Lang.bind(a, a.getFoo), 't');
    let task2 = a.getFoo.asyncCall(a, 'n'); // preferred

    let success = false;
    task1.get(function(v) { success=(v=='bart'); }, function() {});
    assertTrue(success);

    success = false;
    task2.get(function(v) { success=(v=='barn'); }, function() {});
    assertTrue(success);

    // check to be sure that testMe has 'this' bound properly, too.
    success = false;
    a.testMe(function(v) { success=(v=='bard'); }, function() {}, 'd');
    assertTrue(success);

    // argsAtEnd should leave 'this' unaffected
    success = false;
    a.testEnd('k', function(v) { success=(v=='bark'); }, function() {});
    assertTrue(success);

    success = false;
    let task3 = a.testEnd.asyncCall(a, 'f');
    task3.get(function(v) { success=(v=='barf'); }, function() {});
    assertTrue(success);
}

function testParallel() {
    // this test doesn't really demonstrate that the tasks are started in
    // parallel, but it shows that the basic functionality is correct.
    let getInIdle = Async.asyncFunc(function(onReturn, onError, x) {
        try {
            Mainloop.idle_add(function() { onReturn(x); return false; });
        } catch (e) {
            onError(e);
        }
    });
    let throwInIdle = Async.asyncFunc(function(onReturn, onError, x) {
        try {
            Mainloop.idle_add(function() { onError(new Error(x)); return false; });
        } catch (e) {
            onError(e);
        }
    });
    let force = Async.asyncFunc(function(onReturn, onError, promise) {
        promise.get(onReturn, onError);
    });

    let foo = Async.fromGenerator(function() {
        let promise1 = getInIdle.asyncCall(this, 1);
        let promise2 = getInIdle.asyncCall(this, 2);
        let [b,a] = [(yield promise2), (yield promise1)];
        Async.asyncReturn(a+b);
    });

    let success = false;
    foo(function(r) {
            success = (r==3);
            Mainloop.quit('asyncTest');
        }, function(e) {
            success = false;
            Mainloop.quit('asyncTest');
        });
    Mainloop.run('asyncTest');
    assertTrue(success);

    // here we add a 'force' function which blocks waiting for the given
    // promise.  This *does* test that the functions are executed in
    // parallel: it would deadlock if these were serialized.
    let madeItPartWay = 0;
    let bar = Async.fromGenerator(function() {
        let promise1 = getInIdle.asyncCall(this, 1);
        let promise2 = throwInIdle.asyncCall(this, "testError");
        let promise3 = force.asyncCall(this, promise1);
        yield promise3;
        yield promise1;
        // we should make it to here without deadlocking.
        madeItPartWay = 1;
        // fetching the value of promise2 should throw the exception.
        yield promise2;
        // shouldn't get here.
        madeItPartWay = 2;
        Async.asyncReturn(42);
    });

    success = false;
    bar(function(r) {
            success = false;
            Mainloop.quit('asyncTest');
        }, function(e) {
            success = (e.message=='testError');
            Mainloop.quit('asyncTest');
        });
    Mainloop.run('asyncTest');
    assertTrue(success);
    assertTrue(madeItPartWay==1);
}

function testAsyncConstructor() {
    // we also support asynchronous creation of objects.
    let idleWait = Async.asyncFunc(function(onReturn, onError) {
        try {
            Mainloop.idle_add(function() { onReturn(); return false; });
        } catch (e) {
            onError(e);
        }
    });
    function Foo(onReturn, onError, val) {
        this._init(onReturn, onError, val);
    }
    Foo.asyncNew = Async.asyncNew;
    Foo.prototype = {
        _init : Async.fromGenerator(function(val) {
            // do the initialization in the idle thread
            yield idleWait.asyncCall();
            this.val = val;
        })
    };

    let runTest = Async.fromGenerator(function() {
        let foo = yield Foo.asyncNew(5);
        // all the expected properties of an instance of Foo are present
        assertTrue(typeof foo == "object");
        assertTrue(foo instanceof Foo);
        assertTrue(foo.constructor == Foo);
        Async.asyncReturn(foo.val);
    });

    let success = false;
    runTest(function(r) {
        success = (r==5);
        Mainloop.quit('asyncTest');
    }, function(e) {
        Mainloop.quit('asyncTest');
    });
    Mainloop.run('asyncTest');
    assertTrue(success);
}

gjstestRun();
