// Copyright 2014 the V8 project authors. All rights reserved.
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
//     * Redistributions of source code must retain the above copyright
//       notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above
//       copyright notice, this list of conditions and the following
//       disclaimer in the documentation and/or other materials provided
//       with the distribution.
//     * Neither the name of Google Inc. nor the names of its
//       contributors may be used to endorse or promote products derived
//       from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

// This file emulates Mocha test framework used in promises-aplus tests.

var describe;
var it;
var specify;
var before;
var after;
var beforeEach;
var afterEach;
var RunAllTests;

var assert = require('assert');

(function() {
var TIMEOUT = 1000;

function PostMicrotask(fn) {
  var o = {};
  Object.observe(o, function() {
    fn();
  });
  // Change something to enqueue a microtask.
  o.x = 'hello';
}

var context = {
  beingDescribed: undefined,
  currentSuiteIndex: 0,
  suites: []
};

function Run() {
  function current() {
    while (context.currentSuiteIndex < context.suites.length &&
           context.suites[context.currentSuiteIndex].hasRun) {
      ++context.currentSuiteIndex;
    }
    if (context.suites.length == context.currentSuiteIndex) {
      return undefined;
    }
    return context.suites[context.currentSuiteIndex];
  }
  var suite = current();
  if (!suite) {
    // done
    print('All tests have run.');
    return;
  }
  suite.Run();
}

RunAllTests = function() {
  context.currentSuiteIndex = 0;
  var numRegularTestCases = 0;
  for (var i = 0; i < context.suites.length; ++i) {
    numRegularTestCases += context.suites[i].numRegularTestCases();
  }
  print(context.suites.length + ' suites and ' + numRegularTestCases +
        ' test cases are found');
  Run();
};

function TestCase(name, before, fn, after, isRegular) {
  this.name = name;
  this.before = before;
  this.fn = fn;
  this.after = after;
  this.isRegular = isRegular;
  this.hasDone = false;
}

TestCase.prototype.RunFunction = function(suite, fn, postAction) {
  if (!fn) {
    postAction();
    return;
  }
  try {
    if (fn.length === 0) {
      // synchronous
      fn();
      postAction();
    } else {
      // asynchronous
      fn(postAction);
    }
  } catch (e) {
    suite.ReportError(this, e);
  }
}

TestCase.prototype.MarkAsDone = function() {
  this.hasDone = true;
  clearTimeout(this.timer);
}

TestCase.prototype.Run = function(suite, postAction) {
  print('Running ' + suite.description + '#' + this.name + ' ...');
  assert.clear();

  this.timer = setTimeout(function() {
    suite.ReportError(this, Error('timeout'));
  }.bind(this), TIMEOUT);

  this.RunFunction(suite, this.before, function(e) {
    if (this.hasDone) {
      return;
    }
    if (e instanceof Error) {
      return suite.ReportError(this, e);
    }
    if (assert.fails.length > 0) {
      return suite.ReportError(this, assert.fails[0]);
    }
    this.RunFunction(suite, this.fn, function(e) {
      if (this.hasDone) {
        return;
      }
      if (e instanceof Error) {
        return suite.ReportError(this, e);
      }
      if (assert.fails.length > 0) {
        return suite.ReportError(this, assert.fails[0]);
      }
      this.RunFunction(suite, this.after, function(e) {
        if (this.hasDone) {
          return;
        }
        if (e instanceof Error) {
          return suite.ReportError(this, e);
        }
        if (assert.fails.length > 0) {
          return suite.ReportError(this, assert.fails[0]);
        }
        this.MarkAsDone();
        if (this.isRegular) {
          print('PASS: ' + suite.description + '#' + this.name);
        }
        PostMicrotask(postAction);
      }.bind(this));
    }.bind(this));
  }.bind(this));
};

function TestSuite(described) {
  this.description = described.description;
  this.cases = [];
  this.currentIndex = 0;
  this.hasRun = false;

  if (described.before) {
    this.cases.push(new TestCase(this.description + ' :before', undefined,
                                 described.before, undefined, false));
  }
  for (var i = 0; i < described.cases.length; ++i) {
    this.cases.push(new TestCase(described.cases[i].description,
                                 described.beforeEach,
                                 described.cases[i].fn,
                                 described.afterEach,
                                 true));
  }
  if (described.after) {
    this.cases.push(new TestCase(this.description + ' :after',
                                 undefined, described.after, undefined, false));
  }
}

TestSuite.prototype.Run = function() {
  this.hasRun = this.currentIndex === this.cases.length;
  if (this.hasRun) {
    PostMicrotask(Run);
    return;
  }

  // TestCase.prototype.Run cannot throw an exception.
  this.cases[this.currentIndex].Run(this, function() {
    ++this.currentIndex;
    PostMicrotask(Run);
  }.bind(this));
};

TestSuite.prototype.numRegularTestCases = function() {
  var n = 0;
  for (var i = 0; i < this.cases.length; ++i) {
    if (this.cases[i].isRegular) {
      ++n;
    }
  }
  return n;
}

TestSuite.prototype.ReportError = function(testCase, e) {
  if (testCase.hasDone) {
    return;
  }
  testCase.MarkAsDone();
  this.hasRun = this.currentIndex === this.cases.length;
  print('FAIL: ' + this.description + '#' + testCase.name + ': ' +
        e.name  + ' (' + e.message + ')');
  ++this.currentIndex;
  PostMicrotask(Run);
};

describe = function(description, fn) {
  var parent = context.beingDescribed;
  var incomplete = {
    cases: [],
    description: parent ? parent.description + ' ' + description : description,
    parent: parent,
  };
  context.beingDescribed = incomplete;
  fn();
  context.beingDescribed = parent;

  context.suites.push(new TestSuite(incomplete));
}

specify = it = function(description, fn) {
  context.beingDescribed.cases.push({description: description, fn: fn});
}

before = function(fn) {
  context.beingDescribed.before = fn;
}

after = function(fn) {
  context.beingDescribed.after = fn;
}

beforeEach = function(fn) {
  context.beingDescribed.beforeEach = fn;
}

afterEach = function(fn) {
  context.beingDescribed.afterEach = fn;
}

}());
