// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

/**
 * @fileoverview Provides wrappers around the following global functions from
 * [Mocha's BDD interface](https://github.com/mochajs/mocha):
 *
 * - after
 * - afterEach
 * - before
 * - beforeEach
 * - it
 * - it.only
 * - it.skip
 * - xit
 *
 * The provided wrappers leverage the {@link webdriver.promise.ControlFlow}
 * to simplify writing asynchronous tests:
 *
 *     var By = require('selenium-webdriver').By,
 *         until = require('selenium-webdriver').until,
 *         firefox = require('selenium-webdriver/firefox'),
 *         test = require('selenium-webdriver/testing');
 *
 *     test.describe('Google Search', function() {
 *       var driver;
 *
 *       test.before(function() {
 *         driver = new firefox.Driver();
 *       });
 *
 *       test.after(function() {
 *         driver.quit();
 *       });
 *
 *       test.it('should append query to title', function() {
 *         driver.get('http://www.google.com/ncr');
 *         driver.findElement(By.name('q')).sendKeys('webdriver');
 *         driver.findElement(By.name('btnG')).click();
 *         driver.wait(until.titleIs('webdriver - Google Search'), 1000);
 *       });
 *     });
 *
 * You may conditionally suppress a test function using the exported
 * "ignore" function. If the provided predicate returns true, the attached
 * test case will be skipped:
 *
 *     test.ignore(maybe()).it('is flaky', function() {
 *       if (Math.random() < 0.5) throw Error();
 *     });
 *
 *     function maybe() { return Math.random() < 0.5; }
 */

var promise = require('..').promise;
var flow = promise.controlFlow();


/**
 * Wraps a function so that all passed arguments are ignored.
 * @param {!Function} fn The function to wrap.
 * @return {!Function} The wrapped function.
 */
function seal(fn) {
  return function() {
    fn();
  };
}


/**
 * Wraps a function on Mocha's BDD interface so it runs inside a
 * webdriver.promise.ControlFlow and waits for the flow to complete before
 * continuing.
 * @param {!Function} globalFn The function to wrap.
 * @return {!Function} The new function.
 */
function wrapped(globalFn) {
  return function() {
    if (arguments.length === 1) {
      return globalFn(makeAsyncTestFn(arguments[0]));
    }
    else if (arguments.length === 2) {
      return globalFn(arguments[0], makeAsyncTestFn(arguments[1]));
    }
    else {
      throw Error('Invalid # arguments: ' + arguments.length);
    }
  };
}

/**
 * Make a wrapper to invoke caller's test function, fn.  Run the test function
 * within a ControlFlow.
 *
 * Should preserve the semantics of Mocha's Runnable.prototype.run (See
 * https://github.com/mochajs/mocha/blob/master/lib/runnable.js#L192)
 *
 * @param {Function} fn
 * @return {Function}
 */
function makeAsyncTestFn(fn) {
  var async = fn.length > 0; // if test function expects a callback, its "async"

  var ret = /** @type {function(this: mocha.Context)}*/ (function(done) {
    var runnable = this.runnable();
    var mochaCallback = runnable.callback;
    runnable.callback = function() {
      flow.reset();
      return mochaCallback.apply(this, arguments);
    };

    var testFn = fn.bind(this);
    flow.execute(function controlFlowExecute() {
      return new promise.Promise(function(fulfill, reject) {
        if (async) {
          // If testFn is async (it expects a done callback), resolve the promise of this
          // test whenever that callback says to.  Any promises returned from testFn are
          // ignored.
          testFn(function testFnDoneCallback(err) {
            if (err) {
              reject(err);
            } else {
              fulfill();
            }
          });
        } else {
          // Without a callback, testFn can return a promise, or it will
          // be assumed to have completed synchronously
          fulfill(testFn());
        }
      }, flow);
    }, runnable.fullTitle()).then(seal(done), done);
  });

  ret.toString = function() {
    return fn.toString();
  };

  return ret;
}


/**
 * Ignores the test chained to this function if the provided predicate returns
 * true.
 * @param {function(): boolean} predicateFn A predicate to call to determine
 *     if the test should be suppressed. This function MUST be synchronous.
 * @return {!Object} An object with wrapped versions of {@link #it()} and
 *     {@link #describe()} that ignore tests as indicated by the predicate.
 */
function ignore(predicateFn) {
  var describe = wrap(exports.xdescribe, exports.describe);
  describe.only = wrap(exports.xdescribe, exports.describe.only);

  var it = wrap(exports.xit, exports.it);
  it.only = wrap(exports.xit, exports.it.only);

  return {
    describe: describe,
    it: it
  };

  function wrap(onSkip, onRun) {
    return function(title, fn) {
      if (predicateFn()) {
        onSkip(title, fn);
      } else {
        onRun(title, fn);
      }
    };
  }
}


// PUBLIC API


/**
 * @return {!promise.ControlFlow} the control flow instance used by this module
 *     to coordinate test actions.
 */
exports.controlFlow = function(){
  return flow;
};


/**
 * Registers a new test suite.
 * @param {string} name The suite name.
 * @param {function()=} fn The suite function, or {@code undefined} to define
 *     a pending test suite.
 */
exports.describe = global.describe;

/**
 * Defines a suppressed test suite.
 * @param {string} name The suite name.
 * @param {function()=} fn The suite function, or {@code undefined} to define
 *     a pending test suite.
 */
exports.xdescribe = global.xdescribe;
exports.describe.skip = global.describe.skip;

/**
 * Register a function to call after the current suite finishes.
 * @param {function()} fn .
 */
exports.after = wrapped(global.after);

/**
 * Register a function to call after each test in a suite.
 * @param {function()} fn .
 */
exports.afterEach = wrapped(global.afterEach);

/**
 * Register a function to call before the current suite starts.
 * @param {function()} fn .
 */
exports.before = wrapped(global.before);

/**
 * Register a function to call before each test in a suite.
 * @param {function()} fn .
 */
exports.beforeEach = wrapped(global.beforeEach);

/**
 * Add a test to the current suite.
 * @param {string} name The test name.
 * @param {function()=} fn The test function, or {@code undefined} to define
 *     a pending test case.
 */
exports.it = wrapped(global.it);

/**
 * An alias for {@link #it()} that flags the test as the only one that should
 * be run within the current suite.
 * @param {string} name The test name.
 * @param {function()=} fn The test function, or {@code undefined} to define
 *     a pending test case.
 */
exports.iit = exports.it.only = wrapped(global.it.only);

/**
 * Adds a test to the current suite while suppressing it so it is not run.
 * @param {string} name The test name.
 * @param {function()=} fn The test function, or {@code undefined} to define
 *     a pending test case.
 */
exports.xit = exports.it.skip = wrapped(global.xit);

exports.ignore = ignore;
