'use strict';

if (self.importScripts) {
  importScripts('/resources/testharness.js');
}

setup({
  allow_uncaught_exception: true
});

//
// Straightforward unhandledrejection tests
//
async_test(function(t) {
  var e = new Error();
  var p;

  onUnhandledSucceed(t, e, function() { return p; });

  p = Promise.reject(e);
}, 'unhandledrejection: from Promise.reject');

async_test(function(t) {
  var e = new Error();
  var p;

  onUnhandledSucceed(t, e, function() { return p; });

  p = new Promise(function(_, reject) {
    reject(e);
  });
}, 'unhandledrejection: from a synchronous rejection in new Promise');

async_test(function(t) {
  var e = new Error();
  var p;

  onUnhandledSucceed(t, e, function() { return p; });

  p = new Promise(function(_, reject) {
    postMessageTask(function() {
      reject(e);
    });
  });
}, 'unhandledrejection: from a task-delayed rejection');

async_test(function(t) {
  var e = new Error();
  var p;

  onUnhandledSucceed(t, e, function() { return p; });

  p = new Promise(function(_, reject) {
    setTimeout(function() {
      reject(e);
    }, 1);
  });
}, 'unhandledrejection: from a setTimeout-delayed rejection');

async_test(function(t) {
  var e = new Error();
  var e2 = new Error();
  var promise2;

  onUnhandledSucceed(t, e2, function() { return promise2; });

  var unreached = t.unreached_func('promise should not be fulfilled');
  promise2 = Promise.reject(e).then(unreached, function(reason) {
    t.step(function() {
      assert_equals(reason, e);
    });
    throw e2;
  });
}, 'unhandledrejection: from a throw in a rejection handler chained off of Promise.reject');

async_test(function(t) {
  var e = new Error();
  var e2 = new Error();
  var promise2;

  onUnhandledSucceed(t, e2, function() { return promise2; });

  var unreached = t.unreached_func('promise should not be fulfilled');
  promise2 = new Promise(function(_, reject) {
    setTimeout(function() {
      reject(e);
    }, 1);
  }).then(unreached, function(reason) {
    t.step(function() {
      assert_equals(reason, e);
    });
    throw e2;
  });
}, 'unhandledrejection: from a throw in a rejection handler chained off of a setTimeout-delayed rejection');

async_test(function(t) {
  var e = new Error();
  var e2 = new Error();
  var promise2;

  onUnhandledSucceed(t, e2, function() { return promise2; });

  var promise = new Promise(function(_, reject) {
    setTimeout(function() {
      reject(e);
      mutationObserverMicrotask(function() {
        var unreached = t.unreached_func('promise should not be fulfilled');
        promise2 = promise.then(unreached, function(reason) {
          t.step(function() {
            assert_equals(reason, e);
          });
          throw e2;
        });
      });
    }, 1);
  });
}, 'unhandledrejection: from a throw in a rejection handler attached one microtask after a setTimeout-delayed rejection');

async_test(function(t) {
  var e = new Error();
  var p;

  onUnhandledSucceed(t, e, function() { return p; });

  p = Promise.resolve().then(function() {
    return Promise.reject(e);
  });
}, 'unhandledrejection: from returning a Promise.reject-created rejection in a fulfillment handler');

async_test(function(t) {
  var e = new Error();
  var p;

  onUnhandledSucceed(t, e, function() { return p; });

  p = Promise.resolve().then(function() {
    throw e;
  });
}, 'unhandledrejection: from a throw in a fulfillment handler');

async_test(function(t) {
  var e = new Error();
  var p;

  onUnhandledSucceed(t, e, function() { return p; });

  p = Promise.resolve().then(function() {
    return new Promise(function(_, reject) {
      setTimeout(function() {
        reject(e);
      }, 1);
    });
  });
}, 'unhandledrejection: from returning a setTimeout-delayed rejection in a fulfillment handler');

async_test(function(t) {
  var e = new Error();
  var p;

  onUnhandledSucceed(t, e, function() { return p; });

  p = Promise.all([Promise.reject(e)]);
}, 'unhandledrejection: from Promise.reject, indirected through Promise.all');

//
// Negative unhandledrejection/rejectionhandled tests with immediate attachment
//

async_test(function(t) {
  var e = new Error();
  var p;

  onUnhandledFail(t, function() { return p; });

  var unreached = t.unreached_func('promise should not be fulfilled');
  p = Promise.reject(e).then(unreached, function() {});
}, 'no unhandledrejection/rejectionhandled: rejection handler attached synchronously to a promise from Promise.reject');

async_test(function(t) {
  var e = new Error();
  var p;

  onUnhandledFail(t, function() { return p; });

  var unreached = t.unreached_func('promise should not be fulfilled');
  p = Promise.all([Promise.reject(e)]).then(unreached, function() {});
}, 'no unhandledrejection/rejectionhandled: rejection handler attached synchronously to a promise from ' +
   'Promise.reject, indirecting through Promise.all');

async_test(function(t) {
  var e = new Error();
  var p;

  onUnhandledFail(t, function() { return p; });

  var unreached = t.unreached_func('promise should not be fulfilled');
  p = new Promise(function(_, reject) {
    reject(e);
  }).then(unreached, function() {});
}, 'no unhandledrejection/rejectionhandled: rejection handler attached synchronously to a synchronously-rejected ' +
   'promise created with new Promise');

async_test(function(t) {
  var e = new Error();
  var p;

  onUnhandledFail(t, function() { return p; });

  var unreached = t.unreached_func('promise should not be fulfilled');
  p = Promise.resolve().then(function() {
    throw e;
  }).then(unreached, function(reason) {
    t.step(function() {
      assert_equals(reason, e);
    });
  });
}, 'no unhandledrejection/rejectionhandled: rejection handler attached synchronously to a promise created from ' +
   'throwing in a fulfillment handler');

async_test(function(t) {
  var e = new Error();
  var p;

  onUnhandledFail(t, function() { return p; });

  var unreached = t.unreached_func('promise should not be fulfilled');
  p = Promise.resolve().then(function() {
    return Promise.reject(e);
  }).then(unreached, function(reason) {
    t.step(function() {
      assert_equals(reason, e);
    });
  });
}, 'no unhandledrejection/rejectionhandled: rejection handler attached synchronously to a promise created from ' +
   'returning a Promise.reject-created promise in a fulfillment handler');

async_test(function(t) {
  var e = new Error();
  var p;

  onUnhandledFail(t, function() { return p; });

  var unreached = t.unreached_func('promise should not be fulfilled');
  p = Promise.resolve().then(function() {
    return new Promise(function(_, reject) {
      setTimeout(function() {
        reject(e);
      }, 1);
    });
  }).then(unreached, function(reason) {
    t.step(function() {
      assert_equals(reason, e);
    });
  });
}, 'no unhandledrejection/rejectionhandled: rejection handler attached synchronously to a promise created from ' +
   'returning a setTimeout-delayed rejection in a fulfillment handler');

async_test(function(t) {
  var e = new Error();
  var p;

  onUnhandledFail(t, function() { return p; });

  postMessageTask(function() {
    p = Promise.resolve().then(function() {
      return Promise.reject(e);
    })
    .catch(function() {});
  });
}, 'no unhandledrejection/rejectionhandled: all inside a queued task, a rejection handler attached synchronously to ' +
   'a promise created from returning a Promise.reject-created promise in a fulfillment handler');

//
// Negative unhandledrejection/rejectionhandled tests with microtask-delayed attachment
//

async_test(function(t) {
  var e = new Error();
  var p;

  onUnhandledFail(t, function() { return p; });

  p = Promise.reject(e);
  mutationObserverMicrotask(function() {
    var unreached = t.unreached_func('promise should not be fulfilled');
    p.then(unreached, function() {});
  });
}, 'delayed handling: a microtask delay before attaching a handler prevents both events (Promise.reject-created ' +
   'promise)');

async_test(function(t) {
  var e = new Error();
  var p;

  onUnhandledFail(t, function() { return p; });

  p = new Promise(function(_, reject) {
    reject(e);
  });
  mutationObserverMicrotask(function() {
    var unreached = t.unreached_func('promise should not be fulfilled');
    p.then(unreached, function() {});
  });
}, 'delayed handling: a microtask delay before attaching a handler prevents both events (immediately-rejected new ' +
   'Promise-created promise)');

async_test(function(t) {
  var e = new Error();
  var p1;
  var p2;

  onUnhandledFail(t, function() { return p1; });
  onUnhandledFail(t, function() { return p2; });

  p1 = new Promise(function(_, reject) {
    mutationObserverMicrotask(function() {
      reject(e);
    });
  });
  p2 = Promise.all([p1]);
  mutationObserverMicrotask(function() {
    var unreached = t.unreached_func('promise should not be fulfilled');
    p2.then(unreached, function() {});
  });
}, 'delayed handling: a microtask delay before attaching the handler, and before rejecting the promise, indirected ' +
   'through Promise.all');

//
// Negative unhandledrejection/rejectionhandled tests with nested-microtask-delayed attachment
//

async_test(function(t) {
  var e = new Error();
  var p;

  onUnhandledFail(t, function() { return p; });

  p = Promise.reject(e);
  mutationObserverMicrotask(function() {
    Promise.resolve().then(function() {
      mutationObserverMicrotask(function() {
        Promise.resolve().then(function() {
          p.catch(function() {});
        });
      });
    });
  });
}, 'microtask nesting: attaching a handler inside a combination of mutationObserverMicrotask + promise microtasks');

async_test(function(t) {
  var e = new Error();
  var p;

  onUnhandledFail(t, function() { return p; });

  postMessageTask(function() {
    p = Promise.reject(e);
    mutationObserverMicrotask(function() {
      Promise.resolve().then(function() {
        mutationObserverMicrotask(function() {
          Promise.resolve().then(function() {
            p.catch(function() {});
          });
        });
      });
    });
  });
}, 'microtask nesting: attaching a handler inside a combination of mutationObserverMicrotask + promise microtasks, ' +
   'all inside a postMessageTask');

async_test(function(t) {
  var e = new Error();
  var p;

  onUnhandledFail(t, function() { return p; });

  setTimeout(function() {
    p = Promise.reject(e);
    mutationObserverMicrotask(function() {
      Promise.resolve().then(function() {
        mutationObserverMicrotask(function() {
          Promise.resolve().then(function() {
            p.catch(function() {});
          });
        });
      });
    });
  }, 0);
}, 'microtask nesting: attaching a handler inside a combination of mutationObserverMicrotask + promise microtasks, ' +
   'all inside a setTimeout');

async_test(function(t) {
  var e = new Error();
  var p;

  onUnhandledFail(t, function() { return p; });

  p = Promise.reject(e);
  Promise.resolve().then(function() {
    mutationObserverMicrotask(function() {
      Promise.resolve().then(function() {
        mutationObserverMicrotask(function() {
          p.catch(function() {});
        });
      });
    });
  });
}, 'microtask nesting: attaching a handler inside a combination of promise microtasks + mutationObserverMicrotask');

async_test(function(t) {
  var e = new Error();
  var p;

  onUnhandledFail(t, function() { return p; });

  postMessageTask(function() {
    p = Promise.reject(e);
    Promise.resolve().then(function() {
      mutationObserverMicrotask(function() {
        Promise.resolve().then(function() {
          mutationObserverMicrotask(function() {
            p.catch(function() {});
          });
        });
      });
    });
  });
}, 'microtask nesting: attaching a handler inside a combination of promise microtasks + mutationObserverMicrotask, ' +
   'all inside a postMessageTask');

async_test(function(t) {
  var e = new Error();
  var p;

  onUnhandledFail(t, function() { return p; });

  setTimeout(function() {
    p = Promise.reject(e);
    Promise.resolve().then(function() {
      mutationObserverMicrotask(function() {
        Promise.resolve().then(function() {
          mutationObserverMicrotask(function() {
            p.catch(function() {});
          });
        });
      });
    });
  }, 0);
}, 'microtask nesting: attaching a handler inside a combination of promise microtasks + mutationObserverMicrotask, ' +
   'all inside a setTimeout');


// For workers, postMessageTask() involves posting tasks to other threads, so
// the following tests don't work there.

if ('document' in self) {
  //
  // Negative unhandledrejection/rejectionhandled tests with task-delayed attachment
  //

  async_test(function(t) {
    var e = new Error();
    var p;

    onUnhandledFail(t, function() { return p; });

    var _reject;
    p = new Promise(function(_, reject) {
      _reject = reject;
    });
    _reject(e);
    postMessageTask(function() {
      var unreached = t.unreached_func('promise should not be fulfilled');
      p.then(unreached, function() {});
    });
  }, 'delayed handling: a task delay before attaching a handler prevents unhandledrejection');

  async_test(function(t) {
    var e = new Error();
    var p;

    onUnhandledFail(t, function() { return p; });

    p = Promise.reject(e);
    postMessageTask(function() {
      Promise.resolve().then(function() {
        p.catch(function() {});
      });
    });
  }, 'delayed handling: postMessageTask after promise creation/rejection, plus promise microtasks, is not too late to ' +
     'attach a rejection handler');

  async_test(function(t) {
    var e = new Error();
    var p;

    onUnhandledFail(t, function() { return p; });

    postMessageTask(function() {
      Promise.resolve().then(function() {
        Promise.resolve().then(function() {
          Promise.resolve().then(function() {
            Promise.resolve().then(function() {
              p.catch(function() {});
            });
          });
        });
      });
    });
    p = Promise.reject(e);
  }, 'delayed handling: postMessageTask before promise creation/rejection, plus many promise microtasks, is not too ' +
     'late to attach a rejection handler');

  async_test(function(t) {
    var e = new Error();
    var p;

    onUnhandledFail(t, function() { return p; });

    p = Promise.reject(e);
    postMessageTask(function() {
      Promise.resolve().then(function() {
        Promise.resolve().then(function() {
          Promise.resolve().then(function() {
            Promise.resolve().then(function() {
              p.catch(function() {});
            });
          });
        });
      });
    });
  }, 'delayed handling: postMessageTask after promise creation/rejection, plus many promise microtasks, is not too ' +
     'late to attach a rejection handler');
}

//
// Positive unhandledrejection/rejectionhandled tests with delayed attachment
//

async_test(function(t) {
  var e = new Error();
  var p;

  onUnhandledSucceed(t, e, function() { return p; });

  var _reject;
  p = new Promise(function(_, reject) {
    _reject = reject;
  });
  _reject(e);
  postMessageTask(function() {
    postMessageTask(function() {
      var unreached = t.unreached_func('promise should not be fulfilled');
      p.then(unreached, function() {});
    });
  });
}, 'delayed handling: a nested-task delay before attaching a handler causes unhandledrejection');

async_test(function(t) {
  var e = new Error();
  var p;

  onUnhandledSucceed(t, e, function() { return p; });

  p = Promise.reject(e);
  postMessageTask(function() {
    postMessageTask(function() {
      Promise.resolve().then(function() {
        p.catch(function() {});
      });
    });
  });
}, 'delayed handling: a nested-postMessageTask after promise creation/rejection, plus promise microtasks, is too ' +
   'late to attach a rejection handler');

async_test(function(t) {
  var e = new Error();
  var p;

  onUnhandledSucceed(t, e, function() { return p; });

  postMessageTask(function() {
    postMessageTask(function() {
      Promise.resolve().then(function() {
        Promise.resolve().then(function() {
          Promise.resolve().then(function() {
            Promise.resolve().then(function() {
              p.catch(function() {});
            });
          });
        });
      });
    });
  });
  p = Promise.reject(e);
}, 'delayed handling: a nested-postMessageTask before promise creation/rejection, plus many promise microtasks, is ' +
   'too late to attach a rejection handler');

async_test(function(t) {
  var e = new Error();
  var p;

  onUnhandledSucceed(t, e, function() { return p; });

  p = Promise.reject(e);
  postMessageTask(function() {
    postMessageTask(function() {
      Promise.resolve().then(function() {
        Promise.resolve().then(function() {
          Promise.resolve().then(function() {
            Promise.resolve().then(function() {
              p.catch(function() {});
            });
          });
        });
      });
    });
  });
}, 'delayed handling: a nested-postMessageTask after promise creation/rejection, plus many promise microtasks, is ' +
   'too late to attach a rejection handler');

async_test(function(t) {
  var unhandledPromises = [];
  var unhandledReasons = [];
  var e = new Error();
  var p;

  var unhandled = function(ev) {
    if (ev.promise === p) {
      t.step(function() {
        unhandledPromises.push(ev.promise);
        unhandledReasons.push(ev.reason);
      });
    }
  };
  var handled = function(ev) {
    if (ev.promise === p) {
      t.step(function() {
        assert_array_equals(unhandledPromises, [p]);
        assert_array_equals(unhandledReasons, [e]);
        assert_equals(ev.promise, p);
        assert_equals(ev.reason, e);
      });
    }
  };
  addEventListener('unhandledrejection', unhandled);
  addEventListener('rejectionhandled', handled);
  ensureCleanup(t, unhandled, handled);

  p = new Promise(function() {
    throw e;
  });
  setTimeout(function() {
    var unreached = t.unreached_func('promise should not be fulfilled');
    p.then(unreached, function(reason) {
      assert_equals(reason, e);
      setTimeout(function() { t.done(); }, 10);
    });
  }, 10);
}, 'delayed handling: delaying handling by setTimeout(,10) will cause both events to fire');

//
// Miscellaneous tests about integration with the rest of the platform
//

async_test(function(t) {
  var e = new Error();
  var l = function(ev) {
    var order = [];
    mutationObserverMicrotask(function() {
      order.push(1);
    });
    setTimeout(function() {
      order.push(2);
      t.step(function() {
        assert_array_equals(order, [1, 2]);
      });
      t.done();
    }, 1);
  };
  addEventListener('unhandledrejection', l);
  ensureCleanup(t, l);
  Promise.reject(e);
}, 'mutationObserverMicrotask vs. postMessageTask ordering is not disturbed inside unhandledrejection events');

// For workers, postMessageTask() involves posting tasks to other threads, so
// the following tests don't work there.

if ('document' in self) {

  // For the next two see https://github.com/domenic/unhandled-rejections-browser-spec/issues/2#issuecomment-121121695
  // and the following comments.

  async_test(function(t) {
    var sequenceOfEvents = [];

    addEventListener('unhandledrejection', l);
    ensureCleanup(t, l);

    var p1 = Promise.reject();
    var p2;
    postMessageTask(function() {
      p2 = Promise.reject();
      postMessageTask(function() {
        sequenceOfEvents.push('postMessageTask');
        checkSequence();
      });
    });

    function l(ev) {
      if (ev.promise === p1 || ev.promise === p2) {
        sequenceOfEvents.push(ev.promise);
        checkSequence();
      }
    }

    function checkSequence() {
      if (sequenceOfEvents.length === 3) {
        t.step(function() {
          assert_array_equals(sequenceOfEvents, [p1, 'postMessageTask', p2]);
        });
        t.done();
      }
    }
  }, 'postMessageTask ordering vs. the task queued for unhandled rejection notification (1)');

  async_test(function(t) {
    var sequenceOfEvents = [];

    addEventListener('unhandledrejection', l);
    ensureCleanup(t, l);

    var p2;
    postMessageTask(function() {
      p2 = Promise.reject();
      postMessageTask(function() {
        sequenceOfEvents.push('postMessageTask');
        checkSequence();
      });
    });

    function l(ev) {
      if (ev.promise == p2) {
        sequenceOfEvents.push(ev.promise);
        checkSequence();
      }
    }

    function checkSequence() {
      if (sequenceOfEvents.length === 2) {
        t.step(function() {
          assert_array_equals(sequenceOfEvents, ['postMessageTask', p2]);
        });
        t.done();
      }
    }
  }, 'postMessageTask ordering vs. the task queued for unhandled rejection notification (2)');

  async_test(function(t) {
    var sequenceOfEvents = [];


    addEventListener('unhandledrejection', unhandled);
    addEventListener('rejectionhandled', handled);
    ensureCleanup(t, unhandled, handled);

    var p = Promise.reject();

    setTimeout(function() {
      postMessageTask(function() {
        sequenceOfEvents.push('task before catch');
        checkSequence();
      });

      p.catch(function() {
        sequenceOfEvents.push('catch');
        checkSequence();
      });

      postMessageTask(function() {
        sequenceOfEvents.push('task after catch');
        checkSequence();
      });

      sequenceOfEvents.push('after catch');
      checkSequence();
    }, 10);

    function unhandled(ev) {
      if (ev.promise === p) {
        sequenceOfEvents.push('unhandled');
        checkSequence();
      }
    }

    function handled(ev) {
      if (ev.promise === p) {
        sequenceOfEvents.push('handled');
        checkSequence();
      }
    }

    function checkSequence() {
      if (sequenceOfEvents.length === 6) {
        t.step(function() {
          assert_array_equals(sequenceOfEvents,
            ['unhandled', 'after catch', 'catch', 'task before catch', 'handled', 'task after catch']);
        });
        t.done();
      }
    }
  }, 'rejectionhandled is dispatched from a queued task, and not immediately');
}

//
// HELPERS
//

var globalPostMessageCounter = 0;

function postMessageTask(f) {
  if ('document' in self) {
    var message = 'abusingpostmessageforfunandprofit' + globalPostMessageCounter;
    globalPostMessageCounter++;
    var l = function(ev) {
      if (ev.data === message) {
        removeEventListener('message', l);
        f();
      }
    };
    addEventListener('message', l);
    postMessage(message, '*');
  } else {
    var channel = new MessageChannel();
    channel.port1.onmessage = function() { channel.port1.close(); f(); };
    channel.port2.postMessage('abusingpostmessageforfunandprofit');
    channel.port2.close();
  }
}

function mutationObserverMicrotask(f) {
  if ('document' in self) {
    var observer = new MutationObserver(function() { f(); });
    var node = document.createTextNode('');
    observer.observe(node, { characterData: true });
    node.data = 'foo';
  } else {
    // We don't have mutation observers on workers, so just post a promise-based
    // microtask.
    Promise.resolve().then(function() { f(); });
  }
}

function onUnhandledSucceed(t, expectedReason, expectedPromiseGetter) {
  var l = function(ev) {
    if (ev.promise === expectedPromiseGetter()) {
      t.step(function() {
        assert_equals(ev.reason, expectedReason);
        assert_equals(ev.promise, expectedPromiseGetter());
      });
      t.done();
    }
  };
  addEventListener('unhandledrejection', l);
  ensureCleanup(t, l);
}

function onUnhandledFail(t, expectedPromiseGetter) {
  var unhandled = function(evt) {
    if (evt.promise === expectedPromiseGetter()) {
      t.step(function() {
        assert_unreached('unhandledrejection event is not supposed to be triggered');
      });
    }
  };
  var handled = function(evt) {
    if (evt.promise === expectedPromiseGetter()) {
      t.step(function() {
        assert_unreached('rejectionhandled event is not supposed to be triggered');
      });
    }
  };
  addEventListener('unhandledrejection', unhandled);
  addEventListener('rejectionhandled', handled);
  ensureCleanup(t, unhandled, handled);
  setTimeout(function() {
    t.done();
  }, 10);
}

function ensureCleanup(t, unhandled, handled) {
  t.add_cleanup(function() {
    if (unhandled)
      removeEventListener('unhandledrejection', unhandled);
    if (handled)
      removeEventListener('rejectionhandled', handled);
  });
}

done();
