let Cu = Components.utils;

Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/AsyncShutdown.jsm");

Services.prefs.setBoolPref("toolkit.asyncshutdown.testing", true);

function run_test() {
  run_next_test();
}

function get_exn(f) {
  try {
    f();
    return null;
  } catch (ex) {
    return ex;
  }
}

function do_check_exn(exn, constructor) {
  do_check_neq(exn, null);
  if (exn.name == constructor) {
    do_check_eq(exn.constructor.name, constructor);
    return;
  }
  do_print("Wrong error constructor");
  do_print(exn.constructor.name);
  do_print(exn.stack);
  do_check_true(false);
}

/**
 * Utility function used to provide the same API for both AsyncShutdown
 * phases and AsyncShutdown barriers.
 *
 * @param {bool} heavy If |true|, use a AsyncShutdown phase, otherwise
 * a AsyncShutdown barrier.
 *
 * @return An object with the following methods:
 *   - addBlocker() - the same method as AsyncShutdown phases and barrier clients
 *   - wait() - trigger the resolution of the lock
 */
function makeLock(heavy) {
  if (heavy) {
    let topic = "test-Phase-" + ++makeLock.counter;
    let phase = AsyncShutdown._getPhase(topic);
    return {
      addBlocker: function(...args) {
        return phase.addBlocker(...args);
      },
      removeBlocker: function(blocker) {
        return phase.removeBlocker(blocker);
      },
      wait: function() {
        Services.obs.notifyObservers(null, topic, null);
        return Promise.resolve();
      }
    };
  } else {
    let name = "test-Barrier-" + ++makeLock.counter;
    let barrier = new AsyncShutdown.Barrier(name);
    return {
      addBlocker: barrier.client.addBlocker,
      removeBlocker: barrier.client.removeBlocker,
      wait: function() {
        return barrier.wait();
      }
    };
  }
}
makeLock.counter = 0;

/**
 * An asynchronous task that takes several ticks to complete.
 *
 * @param {*=} resolution The value with which the resulting promise will be
 * resolved once the task is complete. This may be a rejected promise,
 * in which case the resulting promise will itself be rejected.
 * @param {object=} outResult An object modified by side-effect during the task.
 * Initially, its field |isFinished| is set to |false|. Once the task is
 * complete, its field |isFinished| is set to |true|.
 *
 * @return {promise} A promise fulfilled once the task is complete
 */
function longRunningAsyncTask(resolution = undefined, outResult = {}) {
  outResult.isFinished = false;
  if (!("countFinished" in outResult)) {
    outResult.countFinished = 0;
  }
  let deferred = Promise.defer();
  do_timeout(100, function() {
    ++outResult.countFinished;
    outResult.isFinished = true;
    deferred.resolve(resolution);
  });
  return deferred.promise;
}


//////// Tests on AsyncShutdown phases

/*add_task*/(function* test_no_condition() {
  for (let heavy of [false, true]) {
    do_print("Testing a barrier with no condition (" + heavy?"heavy":"light" + ")");
    let lock = makeLock(heavy);
    yield lock.wait();
    do_print("Barrier with no condition didn't lock");
  }
});

/*add_task*/(function* test_phase_simple_async() {
  do_print("Testing various combinations of a phase with a single condition");
  for (let heavy of [false, true]) {
    for (let arg of [undefined, null, "foo", 100, new Error("BOOM")]) {
      for (let resolution of [arg, Promise.reject(arg)]) {
        for (let success of [false, true]) {
          for (let state of [[null],
                             [],
                             [() => "some state"],
                             [function() {
                               throw new Error("State BOOM"); }],
                             [function() {
                               return {
                                 toJSON: function() {
                                   throw new Error("State.toJSON BOOM");
                                 }
                               };
                             }]]) {
            // Asynchronous phase
            do_print("Asynchronous test with " + arg + ", " + resolution + ", " + heavy);
            let lock = makeLock(heavy);
            let outParam = { isFinished: false };
            lock.addBlocker(
              "Async test",
               function() {
                 if (success) {
                   return longRunningAsyncTask(resolution, outParam);
                 } else {
                   throw resolution;
                 }
               },
               ...state
            );
            do_check_false(outParam.isFinished);
            yield lock.wait();
            do_check_eq(outParam.isFinished, success);
          }
        }

        // Synchronous phase - just test that we don't throw/freeze
        do_print("Synchronous test with " + arg + ", " + resolution + ", " + heavy);
        let lock = makeLock(heavy);
        lock.addBlocker(
          "Sync test",
          resolution
        );
        yield lock.wait();
      }
    }
  }
});

/*add_task*/(function* test_phase_many() {
  do_print("Testing various combinations of a phase with many conditions");
  for (let heavy of [false, true]) {
    let lock = makeLock(heavy);
    let outParams = [];
    for (let arg of [undefined, null, "foo", 100, new Error("BOOM")]) {
      for (let resolve of [true, false]) {
        do_print("Testing with " + heavy + ", " + arg + ", " + resolve);
        let resolution = resolve ? arg : Promise.reject(arg);
        let outParam = { isFinished: false };
        lock.addBlocker(
          "Test",
          () => longRunningAsyncTask(resolution, outParam)
        );
      }
    }
    do_check_true(outParams.every((x) => !x.isFinished));
    yield lock.wait();
    do_check_true(outParams.every((x) => x.isFinished));
  }
});



/*add_task*/(function* test_phase_various_failures() {
  do_print("Ensure that we cannot add a condition for a phase once notification has been received");
  for (let heavy of [false, true]) {
    let lock = makeLock(heavy);
    lock.wait(); // Don't actually wait for the promise to be resolved
    let exn = get_exn(() => lock.addBlocker("Test", true));
    do_check_true(!!exn);

    do_print("Ensure that an incomplete blocker causes a TypeError");

    lock = makeLock(heavy);
    exn = get_exn(() => lock.addBlocker());
    do_check_exn(exn, "TypeError");

    exn = get_exn(() => lock.addBlocker(null, true));
    do_check_exn(exn, "TypeError");

    exn = get_exn(() => lock.addBlocker("Test 2", () => true, "not a function"));
    do_check_exn(exn, "TypeError");
  }
});


add_task(function* test_phase_removeBlocker() {
  do_print("Testing that we can call removeBlocker before, during and after the call to wait()");

  for (let heavy of [false, true]) {

    do_print("Attempt to add then remove a blocker before wait()");
    let lock = makeLock(heavy);
    let blocker = () => {
      do_print("This promise will never be resolved");
      return Promise.defer().promise;
    };

    lock.addBlocker("Wait forever", blocker);
    do_check_true(lock.removeBlocker(blocker));
    do_check_false(lock.removeBlocker(blocker));
    do_print("Attempt to remove non-registered blockers before wait()");
    do_check_false(lock.removeBlocker("foo"));
    do_check_false(lock.removeBlocker(null));
    do_print("Waiting (should lift immediately)");
    yield lock.wait();

    do_print("Attempt to add a blocker then remove it during wait()");
    lock = makeLock(heavy);
    let blockers = [
      () => {
        do_print("This blocker will self-destruct");
        do_check_true(lock.removeBlocker(blockers[0]));
        return Promise.defer().promise;
      },
      () => {
        do_print("This blocker will self-destruct twice");
        do_check_true(lock.removeBlocker(blockers[1]));
        do_check_false(lock.removeBlocker(blockers[1]));
        return Promise.defer().promise;
      },
      () => {
        do_print("Attempt to remove non-registered blockers during wait()");
        do_check_false(lock.removeBlocker("foo"));
        do_check_false(lock.removeBlocker(null));
      }
    ];
    for (let i in blockers) {
      lock.addBlocker("Wait forever again: " + i, blockers[i]);
    }
    do_print("Waiting (should lift very quickly)");
    yield lock.wait();
    do_check_false(lock.removeBlocker(blockers[0]));


    do_print("Attempt to remove a blocker after wait");
    lock = makeLock(heavy);
    blocker = Promise.resolve;
    yield lock.wait();

    do_print("Attempt to remove non-registered blocker after wait()");
    do_check_false(lock.removeBlocker("foo"));
    do_check_false(lock.removeBlocker(null));
  }

});

add_task(function* test_state() {
  do_print("Testing information contained in `state`");

  let BLOCKER_NAME = "test_state blocker " + Math.random();

  // Set up the barrier. Note that we cannot test `barrier.state`
  // immediately, as it initially contains "Not started"
  let barrier = new AsyncShutdown.Barrier("test_filename");
  let deferred = Promise.defer();
  let {filename, lineNumber} = Components.stack;
  barrier.client.addBlocker(BLOCKER_NAME,
    function() {
      return deferred.promise;
    });

  let promiseDone = barrier.wait();

  // Now that we have called `wait()`, the state contains interesting things
  let state = barrier.state[0];
  do_print("State: " + JSON.stringify(barrier.state, null, "\t"));
  Assert.equal(state.filename, filename);
  Assert.equal(state.lineNumber, lineNumber + 2);
  Assert.equal(state.name, BLOCKER_NAME);
  
  deferred.resolve();
  yield promiseDone;
});

add_task(function*() {
  Services.prefs.clearUserPref("toolkit.asyncshutdown.testing");
});

