// Copyright 2018 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

// Used for async tests. See definition below for more documentation.
var testAsync;

(function () {  // Scope for utility functions.
  /**
   * This is to be used through the testAsync helper function defined
   * below.
   *
   * This requires the --allow-natives-syntax flag to allow calling
   * runtime functions.
   *
   * There must be at least one assertion in an async test. A test
   * with no assertions will fail.
   *
   * @example
   * testAsync(assert => {
   *   assert.plan(1) // There should be one assertion in this test.
   *   Promise.resolve(1)
   *    .then(val => assert.equals(1, val),
   *          assert.unreachable);
   * })
   */
  class AsyncAssertion {
    constructor(test, name) {
      this.expectedAsserts_ = -1;
      this.actualAsserts_ = 0;
      this.test_ = test;
      this.name_ = name || '';
    }

    /**
     * Sets the number of expected asserts in the test. The test fails
     * if the number of asserts computed after running the test is not
     * equal to this specified value.
     * @param {number} expectedAsserts
     */
    plan(expectedAsserts) {
      this.expectedAsserts_ = expectedAsserts;
    }

    fail(expectedText, found) {
      let message = formatFailureText(expectedText, found);
      message += "\nin test:" + this.name_
      message += "\n" + Function.prototype.toString.apply(this.test_);
      %AbortJS(message);
    }

    equals(expected, found, name_opt) {
      this.actualAsserts_++;
      if (!deepEquals(expected, found)) {
        this.fail(prettyPrinted(expected), found, name_opt);
      }
    }

    unreachable() {
      let message = "Failure: unreachable in test: " + this.name_;
      message += "\n" + Function.prototype.toString.apply(this.test_);
      %AbortJS(message);
    }

    unexpectedRejection(details) {
      return (error) => {
        let message =
            "Failure: unexpected Promise rejection in test: " + this.name_;
        if (details) message += "\n    @" + details;
        if (error instanceof Error) {
          message += "\n" + String(error.stack);
        } else {
          message += "\n" + String(error);
        }
        message += "\n\n" + Function.prototype.toString.apply(this.test_);
        %AbortJS(message);
      };
    }

    drainMicrotasks() {
      %PerformMicrotaskCheckpoint();
    }

    done_() {
      if (this.expectedAsserts_ === -1) {
        let message = "Please call t.plan(count) to initialize test harness " +
            "with correct assert count (Note: count > 0)";
        %AbortJS(message);
      }

      if (this.expectedAsserts_ !== this.actualAsserts_) {
        let message = "Expected asserts: " + this.expectedAsserts_;
        message += ", Actual asserts: " + this.actualAsserts_;
        message += "\nin test: " + this.name_;
        message += "\n" + Function.prototype.toString.apply(this.test_);
        %AbortJS(message);
      }
    }
  }

  /** This is used to test async functions and promises.
   * @param {testCallback} test - test function
   * @param {string} [name] - optional name of the test
   *
   *
   * @callback testCallback
   * @param {AsyncAssertion} assert
   */
  testAsync = function(test, name) {
    let assert = new AsyncAssertion(test, name);
    test(assert);
    %PerformMicrotaskCheckpoint();
    assert.done_();
  }
})();