/*
 * Copyright 2009, Google Inc.
 * 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.
 */


/**
 * @fileoverview This is a simple unit testing library used to test the
 * sample utilities
 *
 *
 */
o3djs.provide('o3djs.test');

/**
 * A unit testing library
 */
o3djs.test = o3djs.test || {};

/**
  * Class of errors thrown by assertions
  * @param {string} message The assertion message.
  * @this o3djs.test.AssertionError
  */
o3djs.test.AssertionError = function(message) {
  this.message = message;

  /**
    * Returns the error message.
    * @return {String} The error message.
    */
  this.toString = function() {
    return message;
  };
};

/**
  * Runs all the tests found in the given suite. Every function with a
  * name beginning with 'test' is considered to be a test.
  * @param {!Object} suite The object containing the test suite.
  * @param {!Object} opt_reporter An optional object to which the results
  *    of the test run are reported.
  * @return {boolean} Whether all the tests passed.
  */
o3djs.test.runTests = function(suite, opt_reporter) {
  try {
    opt_reporter = opt_reporter || o3djs.test.documentReporter;

    var passCount = 0;
    var failCount = 0;
    for (var propertyName in suite) {
      if (propertyName.substring(0, 4) !== 'test')
        continue;

      if (typeof(suite[propertyName]) !== 'function')
        continue;

      try {
        suite[propertyName]();
      } catch (e) {
        ++failCount;
        opt_reporter.reportFail(propertyName, String(e));
        continue;
      }

      ++passCount;
      opt_reporter.reportPass(propertyName);
    }

    opt_reporter.reportSummary(passCount, failCount);
    return failCount == 0;
  }
  catch (e) {
    return false;
  }
};

/**
  * Converts a value to the string representation used in assertion messages.
  * @private
  * @param {*} value The value to convert.
  * @param {number} opt_depth The depth of references to follow for nested
  *     objects. Defaults to 3.
  * @return {string} The string representation.
  */
o3djs.test.valueToString_ = function(value, opt_depth) {
  if (opt_depth === undefined) {
     opt_depth = 3;
  }
  var string;
  if (typeof(value) === 'object') {
    if (value !== null) {
      if (opt_depth === 0) {
        string = '?';
      } else {
        if (o3djs.base.isArray(value)) {
          var valueAsArray = /** @type {!Array.<*>} */ (value);
          string = '[';
          var separator = '';
          for (var i = 0; i < valueAsArray.length; ++i) {
            string += separator +
                o3djs.test.valueToString_(valueAsArray[i], opt_depth - 1);
            separator = ', ';
          }
          string += ']';
        } else {
          var valueAsObject = /** @type {!Object} */ (value);
          string = '{';
          var separator = '';
          for (var propertyName in valueAsObject) {
            if (typeof(valueAsObject[propertyName]) !== 'function') {
              string += separator + propertyName + ': ' +
                  o3djs.test.valueToString_(valueAsObject[propertyName],
                                            opt_depth - 1);
              separator = ', ';
            }
          }
          string += '}';
        }
      }
    } else {
      string = "null";
    }
  } else if (typeof(value) === 'string') {
    string = '"' + value + '"';
  } else {
    string = String(value);
  }
  return string;
};

/**
  * Asserts that a value is true from within a test
  * @param {boolean} value The value to test.
  */
o3djs.test.assertTrue = function(value) {
  if (!value) {
    throw new o3djs.test.AssertionError(
        'assertTrue failed for ' +
            o3djs.test.valueToString_(value));
  }
};

/**
  * Asserts that a value is false from within a test
  * @param {boolean} value The value to test.
  */
o3djs.test.assertFalse = function(value) {
  if (value) {
    throw new o3djs.test.AssertionError(
        'assertFalse failed for ' +
            o3djs.test.valueToString_(value));
  }
};

/**
  * Asserts that a value is null from within a test
  * @param {*} value The value to test.
  */
o3djs.test.assertNull = function(value) {
  if (value !== null) {
    throw new o3djs.test.AssertionError(
        'assertNull failed for ' +
            o3djs.test.valueToString_(value));
  }
};

/**
  * Asserts that an expected value is equal to an actual value.
  * @param {*} expected The expected value.
  * @param {*} actual The actual value.
  */
o3djs.test.assertEquals = function(expected, actual) {
  if (expected !== actual) {
    throw new o3djs.test.AssertionError(
        'assertEquals failed: expected ' +
            o3djs.test.valueToString_(expected) + ' but got ' +
            o3djs.test.valueToString_(actual));
  }
};

/**
  * Asserts that an expected value is close to an actual value
  * within a tolerance of 0.001.
  * @param {number} expected The expected value.
  * @param {number} actual The actual value.
  */
o3djs.test.assertClose = function(expected, actual) {
  if (actual < expected - 0.001 || actual > expected + 0.001) {
    throw new o3djs.test.AssertionError(
        'assertClose failed: expected ' +
            o3djs.test.valueToString_(expected) + ' but got ' +
            o3djs.test.valueToString_(actual));
  }
};

/**
  * Determines whether the elements of a pair of arrays are equal.
  * @private
  * @param {!Array.<*>} expected The expected array.
  * @param {!Array.<*>} actual The actual array.
  * @return {boolean} Whether the arrays are equal.
  */
o3djs.test.compareArrays_ = function(expected, actual) {
  if (expected.length !== actual.length) {
    return false;
  }
  for (var i = 0; i != expected.length; ++i) {
    if (o3djs.base.isArray(expected[i]) && o3djs.base.isArray(actual[i])) {
      var expectedAsArray = /** @type {!Array.<*>} */ (expected[i]);
      var actualAsArray = /** @type {!Array.<*>} */ (actual[i]);
      if (!o3djs.test.compareArrays_(expectedAsArray, actualAsArray)) {
        return false;
      }
    } else if (expected[i] !== actual[i]) {
      return false;
    }
  }
  return true;
};

/**
  * Asserts that an expected array is equal to an actual array.
  * @param {!Array.<*>} expected The expected array.
  * @param {!Array.<*>} actual The actual array.
  */
o3djs.test.assertArrayEquals = function(expected, actual) {
  if (!o3djs.base.isArray(expected)) {
    throw new o3djs.test.AssertionError(
        'assertArrayEquals failed: expected value ' +
            o3djs.test.valueToString_(expected) +
            ' is not an array');
  }
  if (!o3djs.base.isArray(actual)) {
    throw new o3djs.test.AssertionError(
        'assertArrayEquals failed: actual value ' +
            o3djs.test.valueToString_(actual) +
            ' is not an array');
  }
  if (!o3djs.test.compareArrays_(expected, actual)) {
    throw new o3djs.test.AssertionError(
        'assertArrayEquals failed: expected ' +
            o3djs.test.valueToString_(expected) + ' but got ' +
            o3djs.test.valueToString_(actual));
  }
};

/**
 * Creates a DOM paragraph object for the given text and color.
 * @private
 * @param {string} text The text of the message.
 * @param {string} opt_color The optional color of the message.
 * @return {!Element} A DOM paragraph object.
 */
o3djs.test.createReportParagraph_ = function(text, opt_color) {
  var textNode = document.createTextNode(text);
  var paragraph = document.createElement('p');
  paragraph.appendChild(textNode);
  if (opt_color !== undefined) {
    paragraph.style.color = opt_color;
  }
  return paragraph;
};

/**
 * A reporter that reports messages to the document (i.e. the DOM).
 * @type {!Object}
 */
o3djs.test.documentReporter = {
  /**
   * A Report div.
   * @private
   * @this {Object}
   */
  getReportDiv_: function() {
    if (!this.reportDiv_) {
      this.reportDiv_ = document.createElement('div');
      document.body.appendChild(this.reportDiv_);
    }
    return this.reportDiv_;
  },
  /**
   * Reports a test passed.
   * @param {string} testName The name of the test.
   * @this {Object}
   */
  reportPass: function(testName) {
    var paragraph = o3djs.test.createReportParagraph_(
        testName + ' : PASS', 'green');
    this.getReportDiv_().appendChild(paragraph);
  },
  /**
   * Reports a test failed.
   * @param {string} testName The name of the test.
   */
  reportFail: function(testName, message) {
    var paragraph = o3djs.test.createReportParagraph_(
        testName + ' : FAIL : ' + message, 'red');
    var reportDiv = this.getReportDiv_();
    reportDiv.insertBefore(paragraph,
                           reportDiv.firstChild);
  },
  /**
   * Reports a test summary.
   * @param {number} passCount The number of tests that passed.
   * @param {number} failCount The number of tests that failed.
   * @this {Object}
   */
  reportSummary: function(passCount, failCount) {
    var paragraph = o3djs.test.createReportParagraph_(
        passCount + ' passed, ' + failCount + ' failed', 'blue');
    var reportDiv = this.getReportDiv_();
    reportDiv.insertBefore(paragraph,
                           reportDiv.firstChild);
  }
};