// Copyright 2016 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// Flags: --allow-natives-syntax

function assertEqualsAsync(expected, run, msg) {
  var actual;
  var hadValue = false;
  var hadError = false;
  var promise = run();

  if (typeof promise !== "object" || typeof promise.then !== "function") {
    throw new MjsUnitAssertionError(
        "Expected " + run.toString() +
        " to return a Promise, but it returned " + PrettyPrint(promise));
  }

  promise.then(function(value) { hadValue = true; actual = value; },
               function(error) { hadError = true; actual = error; });

  assertFalse(hadValue || hadError);

  %PerformMicrotaskCheckpoint();

  if (hadError) throw actual;

  assertTrue(
      hadValue, "Expected '" + run.toString() + "' to produce a value");

  assertEquals(expected, actual, msg);
};

// Rename a function so that it can help omit things from stack trace.
function test(fn) {
  return Object.defineProperty(fn, "name", {
      enumerable: false,
      configurable: true,
      value: "@" + fn.name,
      writable: false
    });
}

function getStack(error) {
  var stack = error.stack.split('\n').
    filter(function(line) {
        return /^\s*at @?[a-zA-Z0-9_]/.test(line);
    }).
    map(line =>
        line.replace(/^\s*at (@?(?:new )?[a-zA-Z0-9_\.\[\]]+)(.*)/, "$1"));

  // remove `Promise.then()` invocation by assertEqualsAsync()
  if (stack[2] === "assertEqualsAsync") return [];

  return stack.reverse();
}

var log = [];
class FakePromise extends Promise {
  constructor(executor) {
    var stack = getStack(new Error("Getting Callstack"));
    if (stack.length) {
      var first = -1;
      for (var i = 0; i < stack.length; ++i) {
        if (stack[i][0] === '@') {
          first = i;
          break;
        }
      }
      while (first > 0) stack.shift(), --first;
      if (stack.length) {
        log.push("@@Species: [" + stack.join(" > ") + "]");
      }
    }
    return new Promise(executor);
  }
};

Object.defineProperty(Promise, Symbol.species, {
  value: FakePromise,
  configurable: true,
  enumerable: false,
  writable: false
});

// Internal `AsyncFunctionAwait` only --- no @@species invocations.
async function asyncFn() { return await "foo"; }
assertEqualsAsync("foo", test(function testInternalOnly() { return asyncFn(); },
                  "should not call Promise[@@Species]"));
assertEquals([], log);

log.length = 0;
assertEqualsAsync(
    "foo",
    test(function testThenOnReturnedPromise() {
      return asyncFn().then(x => (log.push("Then: " + x), x));
    }),
    "should call Promise[@@Species] after non-internal Then");
assertEquals([
  "@@Species: [@testThenOnReturnedPromise > Promise.then > new FakePromise]",
  "Then: foo"
], log);