"use strict";

/* eslint-disable mozilla/no-comparison-or-assignment-inside-ok */

function ok(a, msg) {
  dump("OK: " + !!a + "  =>  " + a + " " + msg + "\n");
  postMessage({ type: "status", status: !!a, msg: a + ": " + msg });
}

function is(a, b, msg) {
  dump("IS: " + (a === b) + "  =>  " + a + " | " + b + " " + msg + "\n");
  postMessage({
    type: "status",
    status: a === b,
    msg: a + " === " + b + ": " + msg,
  });
}

function isnot(a, b, msg) {
  dump("ISNOT: " + (a !== b) + "  =>  " + a + " | " + b + " " + msg + "\n");
  postMessage({
    type: "status",
    status: a !== b,
    msg: a + " !== " + b + ": " + msg,
  });
}

function promiseResolve() {
  ok(Promise, "Promise object should exist");

  var promise = new Promise(function (resolve, reject) {
    ok(resolve, "Promise.resolve exists");
    ok(reject, "Promise.reject exists");

    resolve(42);
  }).then(
    function (what) {
      ok(true, "Then - resolveCb has been called");
      is(what, 42, "ResolveCb received 42");
      runTest();
    },
    function () {
      ok(false, "Then - rejectCb has been called");
      runTest();
    }
  );
}

function promiseResolveNoArg() {
  var promise = new Promise(function (resolve, reject) {
    ok(resolve, "Promise.resolve exists");
    ok(reject, "Promise.reject exists");

    resolve();
  }).then(
    function (what) {
      ok(true, "Then - resolveCb has been called");
      is(what, undefined, "ResolveCb received undefined");
      runTest();
    },
    function () {
      ok(false, "Then - rejectCb has been called");
      runTest();
    }
  );
}

function promiseRejectNoHandler() {
  // This test only checks that the code that reports unhandled errors in the
  // Promises implementation does not crash or leak.
  var promise = new Promise(function (res, rej) {
    noSuchMethod();
  });
  runTest();
}

function promiseReject() {
  var promise = new Promise(function (resolve, reject) {
    reject(42);
  }).then(
    function (what) {
      ok(false, "Then - resolveCb has been called");
      runTest();
    },
    function (what) {
      ok(true, "Then - rejectCb has been called");
      is(what, 42, "RejectCb received 42");
      runTest();
    }
  );
}

function promiseRejectNoArg() {
  var promise = new Promise(function (resolve, reject) {
    reject();
  }).then(
    function (what) {
      ok(false, "Then - resolveCb has been called");
      runTest();
    },
    function (what) {
      ok(true, "Then - rejectCb has been called");
      is(what, undefined, "RejectCb received undefined");
      runTest();
    }
  );
}

function promiseException() {
  var promise = new Promise(function (resolve, reject) {
    throw 42;
  }).then(
    function (what) {
      ok(false, "Then - resolveCb has been called");
      runTest();
    },
    function (what) {
      ok(true, "Then - rejectCb has been called");
      is(what, 42, "RejectCb received 42");
      runTest();
    }
  );
}

function promiseAsync_TimeoutResolveThen() {
  var handlerExecuted = false;

  setTimeout(function () {
    ok(handlerExecuted, "Handler should have been called before the timeout.");

    // Allow other assertions to run so the test could fail before the next one.
    setTimeout(runTest, 0);
  }, 0);

  Promise.resolve().then(function () {
    handlerExecuted = true;
  });

  ok(!handlerExecuted, "Handlers are not called before 'then' returns.");
}

function promiseAsync_ResolveTimeoutThen() {
  var handlerExecuted = false;

  var promise = Promise.resolve();

  setTimeout(function () {
    ok(handlerExecuted, "Handler should have been called before the timeout.");

    // Allow other assertions to run so the test could fail before the next one.
    setTimeout(runTest, 0);
  }, 0);

  promise.then(function () {
    handlerExecuted = true;
  });

  ok(!handlerExecuted, "Handlers are not called before 'then' returns.");
}

function promiseAsync_ResolveThenTimeout() {
  var handlerExecuted = false;

  Promise.resolve().then(function () {
    handlerExecuted = true;
  });

  setTimeout(function () {
    ok(handlerExecuted, "Handler should have been called before the timeout.");

    // Allow other assertions to run so the test could fail before the next one.
    setTimeout(runTest, 0);
  }, 0);

  ok(!handlerExecuted, "Handlers are not called before 'then' returns.");
}

function promiseAsync_SyncXHRAndImportScripts() {
  var handlerExecuted = false;

  Promise.resolve().then(function () {
    handlerExecuted = true;

    // Allow other assertions to run so the test could fail before the next one.
    setTimeout(runTest, 0);
  });

  ok(!handlerExecuted, "Handlers are not called until the next microtask.");

  var xhr = new XMLHttpRequest();
  xhr.open("GET", "testXHR.txt", false);
  xhr.send(null);

  ok(!handlerExecuted, "Sync XHR should not trigger microtask execution.");

  importScripts("../../../dom/xhr/tests/relativeLoad_import.js");

  ok(!handlerExecuted, "importScripts should not trigger microtask execution.");
}

function promiseDoubleThen() {
  var steps = 0;
  var promise = new Promise(function (r1, r2) {
    r1(42);
  });

  promise.then(
    function (what) {
      ok(true, "Then.resolve has been called");
      is(what, 42, "Value == 42");
      steps++;
    },
    function (what) {
      ok(false, "Then.reject has been called");
    }
  );

  promise.then(
    function (what) {
      ok(true, "Then.resolve has been called");
      is(steps, 1, "Then.resolve - step == 1");
      is(what, 42, "Value == 42");
      runTest();
    },
    function (what) {
      ok(false, "Then.reject has been called");
    }
  );
}

function promiseThenException() {
  var promise = new Promise(function (resolve, reject) {
    resolve(42);
  });

  promise
    .then(function (what) {
      ok(true, "Then.resolve has been called");
      throw "booh";
    })
    .catch(function (e) {
      ok(true, "Catch has been called!");
      runTest();
    });
}

function promiseThenCatchThen() {
  var promise = new Promise(function (resolve, reject) {
    resolve(42);
  });

  var promise2 = promise.then(
    function (what) {
      ok(true, "Then.resolve has been called");
      is(what, 42, "Value == 42");
      return what + 1;
    },
    function (what) {
      ok(false, "Then.reject has been called");
    }
  );

  isnot(promise, promise2, "These 2 promise objs are different");

  promise2
    .then(
      function (what) {
        ok(true, "Then.resolve has been called");
        is(what, 43, "Value == 43");
        return what + 1;
      },
      function (what) {
        ok(false, "Then.reject has been called");
      }
    )
    .catch(function () {
      ok(false, "Catch has been called");
    })
    .then(
      function (what) {
        ok(true, "Then.resolve has been called");
        is(what, 44, "Value == 44");
        runTest();
      },
      function (what) {
        ok(false, "Then.reject has been called");
      }
    );
}

function promiseRejectThenCatchThen() {
  var promise = new Promise(function (resolve, reject) {
    reject(42);
  });

  var promise2 = promise.then(
    function (what) {
      ok(false, "Then.resolve has been called");
    },
    function (what) {
      ok(true, "Then.reject has been called");
      is(what, 42, "Value == 42");
      return what + 1;
    }
  );

  isnot(promise, promise2, "These 2 promise objs are different");

  promise2
    .then(function (what) {
      ok(true, "Then.resolve has been called");
      is(what, 43, "Value == 43");
      return what + 1;
    })
    .catch(function (what) {
      ok(false, "Catch has been called");
    })
    .then(function (what) {
      ok(true, "Then.resolve has been called");
      is(what, 44, "Value == 44");
      runTest();
    });
}

function promiseRejectThenCatchThen2() {
  var promise = new Promise(function (resolve, reject) {
    reject(42);
  });

  promise
    .then(function (what) {
      ok(true, "Then.resolve has been called");
      is(what, 42, "Value == 42");
      return what + 1;
    })
    .catch(function (what) {
      is(what, 42, "Value == 42");
      ok(true, "Catch has been called");
      return what + 1;
    })
    .then(function (what) {
      ok(true, "Then.resolve has been called");
      is(what, 43, "Value == 43");
      runTest();
    });
}

function promiseRejectThenCatchExceptionThen() {
  var promise = new Promise(function (resolve, reject) {
    reject(42);
  });

  promise
    .then(
      function (what) {
        ok(false, "Then.resolve has been called");
      },
      function (what) {
        ok(true, "Then.reject has been called");
        is(what, 42, "Value == 42");
        throw what + 1;
      }
    )
    .catch(function (what) {
      ok(true, "Catch has been called");
      is(what, 43, "Value == 43");
      return what + 1;
    })
    .then(function (what) {
      ok(true, "Then.resolve has been called");
      is(what, 44, "Value == 44");
      runTest();
    });
}

function promiseThenCatchOrderingResolve() {
  var global = 0;
  var f = new Promise(function (r1, r2) {
    r1(42);
  });

  f.then(function () {
    f.then(function () {
      global++;
    });
    f.catch(function () {
      global++;
    });
    f.then(function () {
      global++;
    });
    setTimeout(function () {
      is(global, 2, "Many steps... should return 2");
      runTest();
    }, 0);
  });
}

function promiseThenCatchOrderingReject() {
  var global = 0;
  var f = new Promise(function (r1, r2) {
    r2(42);
  });

  f.then(
    function () {},
    function () {
      f.then(function () {
        global++;
      });
      f.catch(function () {
        global++;
      });
      f.then(
        function () {},
        function () {
          global++;
        }
      );
      setTimeout(function () {
        is(global, 2, "Many steps... should return 2");
        runTest();
      }, 0);
    }
  );
}

function promiseThenNoArg() {
  var promise = new Promise(function (resolve, reject) {
    resolve(42);
  });

  var clone = promise.then();
  isnot(promise, clone, "These 2 promise objs are different");
  promise.then(function (v) {
    clone.then(function (cv) {
      is(v, cv, "Both resolve to the same value");
      runTest();
    });
  });
}

function promiseThenUndefinedResolveFunction() {
  var promise = new Promise(function (resolve, reject) {
    reject(42);
  });

  try {
    promise.then(undefined, function (v) {
      is(v, 42, "Promise rejected with 42");
      runTest();
    });
  } catch (e) {
    ok(false, "then should not throw on undefined resolve function");
  }
}

function promiseThenNullResolveFunction() {
  var promise = new Promise(function (resolve, reject) {
    reject(42);
  });

  try {
    promise.then(null, function (v) {
      is(v, 42, "Promise rejected with 42");
      runTest();
    });
  } catch (e) {
    ok(false, "then should not throw on null resolve function");
  }
}

function promiseCatchNoArg() {
  var promise = new Promise(function (resolve, reject) {
    reject(42);
  });

  var clone = promise.catch();
  isnot(promise, clone, "These 2 promise objs are different");
  promise.catch(function (v) {
    clone.catch(function (cv) {
      is(v, cv, "Both reject to the same value");
      runTest();
    });
  });
}

function promiseNestedPromise() {
  new Promise(function (resolve, reject) {
    resolve(
      new Promise(function (r) {
        ok(true, "Nested promise is executed");
        r(42);
      })
    );
  }).then(function (value) {
    is(value, 42, "Nested promise is executed and then == 42");
    runTest();
  });
}

function promiseNestedNestedPromise() {
  new Promise(function (resolve, reject) {
    resolve(
      new Promise(function (r) {
        ok(true, "Nested promise is executed");
        r(42);
      }).then(function (what) {
        return what + 1;
      })
    );
  }).then(function (value) {
    is(value, 43, "Nested promise is executed and then == 43");
    runTest();
  });
}

function promiseWrongNestedPromise() {
  new Promise(function (resolve, reject) {
    resolve(
      new Promise(function (r, r2) {
        ok(true, "Nested promise is executed");
        r(42);
      })
    );
    reject(42);
  }).then(
    function (value) {
      is(value, 42, "Nested promise is executed and then == 42");
      runTest();
    },
    function (value) {
      ok(false, "This is wrong");
    }
  );
}

function promiseLoop() {
  new Promise(function (resolve, reject) {
    resolve(
      new Promise(function (r1, r2) {
        ok(true, "Nested promise is executed");
        r1(
          new Promise(function (r3, r4) {
            ok(true, "Nested nested promise is executed");
            r3(42);
          })
        );
      })
    );
  }).then(
    function (value) {
      is(value, 42, "Nested nested promise is executed and then == 42");
      runTest();
    },
    function (value) {
      ok(false, "This is wrong");
    }
  );
}

function promiseStaticReject() {
  var promise = Promise.reject(42).then(
    function (what) {
      ok(false, "This should not be called");
    },
    function (what) {
      is(what, 42, "Value == 42");
      runTest();
    }
  );
}

function promiseStaticResolve() {
  var promise = Promise.resolve(42).then(
    function (what) {
      is(what, 42, "Value == 42");
      runTest();
    },
    function () {
      ok(false, "This should not be called");
    }
  );
}

function promiseResolveNestedPromise() {
  var promise = Promise.resolve(
    new Promise(
      function (r, r2) {
        ok(true, "Nested promise is executed");
        r(42);
      },
      function () {
        ok(false, "This should not be called");
      }
    )
  ).then(
    function (what) {
      is(what, 42, "Value == 42");
      runTest();
    },
    function () {
      ok(false, "This should not be called");
    }
  );
}

function promiseUtilitiesDefined() {
  ok(Promise.all, "Promise.all must be defined when Promise is enabled.");
  ok(Promise.race, "Promise.race must be defined when Promise is enabled.");
  runTest();
}

function promiseAllArray() {
  var p = Promise.all([1, new Date(), Promise.resolve("firefox")]);
  ok(p instanceof Promise, "Return value of Promise.all should be a Promise.");
  p.then(
    function (values) {
      ok(Array.isArray(values), "Resolved value should be an array.");
      is(
        values.length,
        3,
        "Resolved array length should match iterable's length."
      );
      is(values[0], 1, "Array values should match.");
      ok(values[1] instanceof Date, "Array values should match.");
      is(values[2], "firefox", "Array values should match.");
      runTest();
    },
    function () {
      ok(
        false,
        "Promise.all shouldn't fail when iterable has no rejected Promises."
      );
      runTest();
    }
  );
}

function promiseAllWaitsForAllPromises() {
  var arr = [
    new Promise(function (resolve) {
      setTimeout(resolve.bind(undefined, 1), 50);
    }),
    new Promise(function (resolve) {
      setTimeout(resolve.bind(undefined, 2), 10);
    }),
    new Promise(function (resolve) {
      setTimeout(
        resolve.bind(
          undefined,
          new Promise(function (resolve2) {
            resolve2(3);
          })
        ),
        10
      );
    }),
    new Promise(function (resolve) {
      setTimeout(resolve.bind(undefined, 4), 20);
    }),
  ];

  var p = Promise.all(arr);
  p.then(
    function (values) {
      ok(Array.isArray(values), "Resolved value should be an array.");
      is(
        values.length,
        4,
        "Resolved array length should match iterable's length."
      );
      is(values[0], 1, "Array values should match.");
      is(values[1], 2, "Array values should match.");
      is(values[2], 3, "Array values should match.");
      is(values[3], 4, "Array values should match.");
      runTest();
    },
    function () {
      ok(
        false,
        "Promise.all shouldn't fail when iterable has no rejected Promises."
      );
      runTest();
    }
  );
}

function promiseAllRejectFails() {
  var arr = [
    new Promise(function (resolve) {
      setTimeout(resolve.bind(undefined, 1), 50);
    }),
    new Promise(function (resolve, reject) {
      setTimeout(reject.bind(undefined, 2), 10);
    }),
    new Promise(function (resolve) {
      setTimeout(resolve.bind(undefined, 3), 10);
    }),
    new Promise(function (resolve) {
      setTimeout(resolve.bind(undefined, 4), 20);
    }),
  ];

  var p = Promise.all(arr);
  p.then(
    function (values) {
      ok(
        false,
        "Promise.all shouldn't resolve when iterable has rejected Promises."
      );
      runTest();
    },
    function (e) {
      ok(
        true,
        "Promise.all should reject when iterable has rejected Promises."
      );
      is(e, 2, "Rejection value should match.");
      runTest();
    }
  );
}

function promiseRaceEmpty() {
  var p = Promise.race([]);
  ok(p instanceof Promise, "Should return a Promise.");
  // An empty race never resolves!
  runTest();
}

function promiseRaceValuesArray() {
  var p = Promise.race([true, new Date(), 3]);
  ok(p instanceof Promise, "Should return a Promise.");
  p.then(
    function (winner) {
      is(winner, true, "First value should win.");
      runTest();
    },
    function (err) {
      ok(false, "Should not fail " + err + ".");
      runTest();
    }
  );
}

function promiseRacePromiseArray() {
  var arr = [
    new Promise(function (resolve) {
      resolve("first");
    }),
    Promise.resolve("second"),
    new Promise(function () {}),
    new Promise(function (resolve) {
      setTimeout(function () {
        setTimeout(function () {
          resolve("fourth");
        }, 0);
      }, 0);
    }),
  ];

  var p = Promise.race(arr);
  p.then(function (winner) {
    is(winner, "first", "First queued resolution should win the race.");
    runTest();
  });
}

function promiseRaceReject() {
  var p = Promise.race([
    Promise.reject(new Error("Fail bad!")),
    new Promise(function (resolve) {
      setTimeout(resolve, 0);
    }),
  ]);

  p.then(
    function () {
      ok(false, "Should not resolve when winning Promise rejected.");
      runTest();
    },
    function (e) {
      ok(true, "Should be rejected");
      ok(e instanceof Error, "Should reject with Error.");
      ok(e.message == "Fail bad!", "Message should match.");
      runTest();
    }
  );
}

function promiseRaceThrow() {
  var p = Promise.race([
    new Promise(function (resolve) {
      nonExistent();
    }),
    new Promise(function (resolve) {
      setTimeout(resolve, 0);
    }),
  ]);

  p.then(
    function () {
      ok(false, "Should not resolve when winning Promise had an error.");
      runTest();
    },
    function (e) {
      ok(true, "Should be rejected");
      ok(
        e instanceof ReferenceError,
        "Should reject with ReferenceError for function nonExistent()."
      );
      runTest();
    }
  );
}

function promiseResolveArray() {
  var p = Promise.resolve([1, 2, 3]);
  ok(p instanceof Promise, "Should return a Promise.");
  p.then(function (v) {
    ok(Array.isArray(v), "Resolved value should be an Array");
    is(v.length, 3, "Length should match");
    is(v[0], 1, "Resolved value should match original");
    is(v[1], 2, "Resolved value should match original");
    is(v[2], 3, "Resolved value should match original");
    runTest();
  });
}

function promiseResolveThenable() {
  var p = Promise.resolve({
    then(onFulfill, onReject) {
      onFulfill(2);
    },
  });
  ok(p instanceof Promise, "Should cast to a Promise.");
  p.then(
    function (v) {
      is(v, 2, "Should resolve to 2.");
      runTest();
    },
    function (e) {
      ok(false, "promiseResolveThenable should've resolved");
      runTest();
    }
  );
}

function promiseResolvePromise() {
  var original = Promise.resolve(true);
  var cast = Promise.resolve(original);

  ok(cast instanceof Promise, "Should cast to a Promise.");
  is(cast, original, "Should return original Promise.");
  cast.then(function (v) {
    is(v, true, "Should resolve to true.");
    runTest();
  });
}

// Bug 1009569.
// Ensure that thenables are run on a clean stack asynchronously.
// Test case adopted from
// https://gist.github.com/getify/d64bb01751b50ed6b281#file-bug1-js.
function promiseResolveThenableCleanStack() {
  function immed(s) {
    x++;
    s();
  }
  function incX() {
    x++;
  }

  var x = 0;
  var thenable = { then: immed };
  var results = [];

  var p = Promise.resolve(thenable).then(incX);
  results.push(x);

  // check what happens after all "next cycle" steps
  // have had a chance to complete
  setTimeout(function () {
    // Result should be [0, 2] since `thenable` will be called async.
    is(results[0], 0, "Expected thenable to be called asynchronously");
    // See Bug 1023547 comment 13 for why this check has to be gated on p.
    p.then(function () {
      results.push(x);
      is(results[1], 2, "Expected thenable to be called asynchronously");
      runTest();
    });
  }, 1000);
}

// Bug 1062323
function promiseWrapperAsyncResolution() {
  var p = new Promise(function (resolve, reject) {
    resolve();
  });

  var results = [];
  var q = p
    .then(function () {
      results.push("1-1");
    })
    .then(function () {
      results.push("1-2");
    })
    .then(function () {
      results.push("1-3");
    });

  var r = p
    .then(function () {
      results.push("2-1");
    })
    .then(function () {
      results.push("2-2");
    })
    .then(function () {
      results.push("2-3");
    });

  Promise.all([q, r]).then(
    function () {
      var match =
        results[0] == "1-1" &&
        results[1] == "2-1" &&
        results[2] == "1-2" &&
        results[3] == "2-2" &&
        results[4] == "1-3" &&
        results[5] == "2-3";
      ok(match, "Chained promises should resolve asynchronously.");
      runTest();
    },
    function () {
      ok(false, "promiseWrapperAsyncResolution: One of the promises failed.");
      runTest();
    }
  );
}

var tests = [
  promiseResolve,
  promiseReject,
  promiseException,
  promiseAsync_TimeoutResolveThen,
  promiseAsync_ResolveTimeoutThen,
  promiseAsync_ResolveThenTimeout,
  promiseAsync_SyncXHRAndImportScripts,
  promiseDoubleThen,
  promiseThenException,
  promiseThenCatchThen,
  promiseRejectThenCatchThen,
  promiseRejectThenCatchThen2,
  promiseRejectThenCatchExceptionThen,
  promiseThenCatchOrderingResolve,
  promiseThenCatchOrderingReject,
  promiseNestedPromise,
  promiseNestedNestedPromise,
  promiseWrongNestedPromise,
  promiseLoop,
  promiseStaticReject,
  promiseStaticResolve,
  promiseResolveNestedPromise,
  promiseResolveNoArg,
  promiseRejectNoArg,

  promiseThenNoArg,
  promiseThenUndefinedResolveFunction,
  promiseThenNullResolveFunction,
  promiseCatchNoArg,
  promiseRejectNoHandler,

  promiseUtilitiesDefined,

  promiseAllArray,
  promiseAllWaitsForAllPromises,
  promiseAllRejectFails,

  promiseRaceEmpty,
  promiseRaceValuesArray,
  promiseRacePromiseArray,
  promiseRaceReject,
  promiseRaceThrow,

  promiseResolveArray,
  promiseResolveThenable,
  promiseResolvePromise,

  promiseResolveThenableCleanStack,

  promiseWrapperAsyncResolution,
];

function runTest() {
  if (!tests.length) {
    postMessage({ type: "finish" });
    return;
  }

  var test = tests.shift();
  test();
}

onmessage = function () {
  runTest();
};
