/**

* The exception thrown when a 'fail' is used.
*
* @param message - reason the test failed/aborted
*/

function FailureException(message) {

this.name = 'FailureException';
this.message = message;
this.toString = function() {
    return this.name + ': "' + this.message + '"';
};

}

/**

* Just flat-out fail the test with the given message
*/

function fail(message) {

throw new FailureException(message);

}

/**

* Perform an assertion several times. If the assertion passes before the
* maximum number of iterations, the assertion passes. Otherwise the
* assertion fails
* @param f The function to perform (possibly) multiple times
* @param maxTries (optional) The maximum number of attempts
* @param delay (optional) The amount of time to pause between attempts
*/

function retry() {

var f = arguments[0];
var maxTries = 3;
var delay = 0.5;
if (arguments.length > 1) {
  maxTries = arguments[1];
}
if (arguments.length > 2) {
  delay = arguments[2];
}

var tries = 0;
var exception = null;
while (tries < maxTries) {
  try {
    f();
    return;  // if we get here, our function must have passed (no exceptions)
  }
  catch(e) {
    exception = e;
    tries++;
    UIATarget.localTarget().delay(delay);
  }
}
throw exception;

}

/**

* The exception thrown for all assert* failures.
*
* @param message - reason the assertion failed
*/

function AssertionException(message) {

this.name = 'AssertionException';
this.message = message;
this.toString = function() {
    return this.name + ': "' + this.message + '"';
};

}

/**

* Asserts that the given expression is true and throws an exception with
* a default message, or the optional +message+ parameter
*/

function assertTrue(expression, message) {

if (! expression) {
  if (! message) {
    message = "Assertion failed";
  }
  throw new AssertionException(message);
}

}

/**

* Asserts that the given regular expression matches the result of the
* given message.
* @param pattern - the pattern to match
* @param expression - the expression to match against
* @param message - an optional string message
*/

function assertMatch(regExp, expression, message) {

var defMessage = "'" + expression + "' does not match '" + regExp + "'";
assertTrue(regExp.test(expression), message ? message + ": " + defMessage : defMessage);

}

/**

* Assert that the +received+ object matches the +expected+ object (using
* plain ol' ==). If it doesn't, this method throws an exception with either
* a default message, or the one given as the last (optional) argument
*/

function assertEquals(expected, received, message) {

var defMessage = "Expected <" + expected + "> but received <" + received + ">";
assertTrue(expected == received, message ? message + ": " + defMessage : defMessage);

}

/**

* Assert that the +received+ object does not matches the +expected+ object (using
* plain ol' !=). If it doesn't, this method throws an exception with either
* a default message, or the one given as the last (optional) argument
*/

function assertNotEquals(expected, received, message) {

var defMessage = "Expected not <" + expected + "> but received <" + received + ">";
assertTrue(expected != received, message ? message + ": " + defMessage : defMessage);

}

/**

* Asserts that the given expression is false and otherwise throws an
* exception with a default message, or the optional +message+ parameter
*/

function assertFalse(expression, message) {

assertTrue(! expression, message);

}

/**

* Asserts that the given object is null or UIAElementNil (UIAutomation's
* version of a null stand-in). If the given object is not one of these,
* an exception is thrown with a default message or the given optional
* +message+ parameter.
*/

function assertNull(thingie, message) {

var defMessage = "Expected a null object, but received <" + thingie + ">";
// TODO: string-matching on UIAElementNil makes my tummy feel bad. Fix it.
assertTrue(thingie === null || thingie.toString() == "[object UIAElementNil]",
  message ? message + ": " + defMessage : defMessage);

}

/**

* Asserts that the given object is not null or UIAElementNil (UIAutomation's
* version of a null stand-in). If it is null, an exception is thrown with
* a default message or the given optional +message+ parameter
*/

function assertNotNull(thingie, message) {

var defMessage = "Expected not null object";
assertTrue(thingie !== null && thingie.toString() != "[object UIAElementNil]",
  message ? message + ": " + defMessage : defMessage);

}

function OnPassException(message) {

this.name = 'OnPassException';
this.message = message;
this.toString = function() {
    return this.name + ': "' + this.message + '"';
};

}

function PropertyMismatchException(propName, expected, given) {

this.name = 'PropertyMismatchException';
this.message = propName + ": expected <" + expected + "> given <" + given + ">";
this.toString = function() {
  return this.name + ": " + this.message;
}

}

/**

* Assert that the given definition matches the given element. The
* definition is a JavaScript object whose property hierarchy matches
* the given UIAElement.  Property names in the given definition that match a
* method will cause that method to be invoked and the matching to be performed
* and the result. For example, the UITableView exposes all UITableViewCells through
* the cells() method. You only need to specify a 'cells' property to
* cause the method to be invoked.
*/

function assertElementTree(element, definition) {

var onPass = null;
if (definition.onPass) {
  onPass = definition.onPass;
  delete definition.onPass;
}

try {
  assertPropertiesMatch(definition, element, 0);
}
catch(e) {
  fail(e.toString())
}

if (onPass) {
  try {
    onPass(element);
  }
  catch(e) {
    throw new OnPassException("Failed to execute 'onPass' callback: " + e);
  }
}

}

/**

* Assert that the given window definition matches the current main window. The
* window definition is a JavaScript object whose property hierarchy matches
* the main UIAWindow.  Property names in the given definition that match a
* method will cause that method to be invoked and the matching to be performed
* and the result. For example, the UIAWindow exposes all UITableViews through
* the tableViews() method. You only need to specify a 'tableViews' property to
* cause the method to be invoked.
*
* PROPERTY HIERARCHY Property definitions can be nested as deeply as
* necessary. Matching is done by traversing the same path in the main
* UIAWindow as your screen definition. For example, to make assertions about
* the left and right buttons in a UINavigationBar you can do this:
*
* assertWindow({
*   navigationBar: {
*     leftButton: { name: "Back" },
*     rightButton: ( name: "Done" },
*   }
* });
*
* PROPERTY MATCHERS For each property you wish to make an assertion about, you
* can specify a string, number regular expression or function. Strings and
* numbers are matches using the assertEquals() method. Regular expressions are
* matches using the assertMatch() method.
*
* If you specify 'null' for a property, it means you don't care to match.
* Typically this is done inside of arrays where you need to match the number
* of elements, but don't necessarily care to make assertions about each one.
*
* Functions are given the matching property as the single argument. For
* example:
*
* assertWindow({
*   navigationBar: {
*     leftButton: function(button) {
*       // make custom assertions here
*     }
*   }
* });
*
* ARRAYS
* If a property you want to match is an array (e.g. tableViews()), you can
* specify one of the above matchers for each element of the array. If the
* number of provided matchers does not match the number of given elements, the
* assertion will fail (throw an exception)
*
* In any case, you specify another object definition for each property to
* drill-down into the atomic properties you wish to test. For example:
*
* assertWindow({
*   navigationBar: {
*     leftButton: { name: "Back" },
*     rightButton: ( name: "Done" },
*   },
*   tableViews: [
*     {
*       groups: [
*         { name: "First Group" },
*         { name: "Second Group" }
*       ],
*       cells: [
*         { name: "Cell 1" },
*         { name: "Cell 2" },
*         { name: "Cell 3" },
*         { name: "Cell 4" }
*       ]
*     }
*   ]
* });
*
* HANDLING FAILURE If any match fails, an appropriate exception will be
* thrown. If you are using the test structure provided by tuneup, this will be
* caught and detailed correctly in Instruments.
*
* POST-PROCESSING If your screen definition provides an 'onPass' property that
* points to a function, that function will be invoked after all matching has
* been peformed on the current window and all assertions have passed. This
* means you can assert the structure of your screen and operate on it in one
* pass:
*
* assertWindow({
*   navigationBar: {
*     leftButton: { name: "Back" }
*   },
*   onPass: function(window) {
*     var leftButton = window.navigationBar().leftButton();
*     leftButton.tap();
*   }
* });
*/

function assertWindow(window) {

target = UIATarget.localTarget();
application = target.frontMostApp();
mainWindow = application.mainWindow();

assertElementTree(mainWindow, window);

}

/**

* Asserts that the +expected+ object matches the +given+ object by making
* assertions appropriate based on the type of each property in the
* +expected+ object. This method will recurse through the structure,
* applying assertions for each matching property path. See the description
* for +assertWindow+ for details on the matchers.
*/

function assertPropertiesMatch(expected, given, level) {

for (var propName in expected) {
  if (expected.hasOwnProperty(propName)) {
    var expectedProp = expected[propName];

    if (propName.match(/~iphone$/)) {
      if (UIATarget.localTarget().model().match(/^iPad/) !== null ||
          UIATarget.localTarget().name().match(/^iPad Simulator$/) !== null) {
        continue;  // we're on the wrong platform, ignore
      }
      else {
        propName = propName.match(/^(.*)~iphone/)[1];
      }
    }
    else if (propName.match(/~ipad$/)) {
      if (UIATarget.localTarget().model().match(/^iPad/) === null &&
          UIATarget.localTarget().name().match(/^iPad Simulator/) === null) {
        continue;  // we're on the wrong platform, ignore
      }
      else {
        propName = propName.match(/^(.*)~ipad/)[1];
      }
    }

    var givenProp = given[propName];

    if (typeof(givenProp) == "function") {
      try {
        // We have to use eval (shudder) because calling functions on
        // UIAutomation objects with () operator crashes
        // See Radar bug 8496138
        givenProp = eval("given." + propName + "()");
      }
      catch (e) {
        UIALogger.logError("[" + propName + "]: Unable to evaluate against " + given);
        continue;
      }
    }

    if (givenProp === null) {
      throw new AssertionException("Could not find given " + given + " property named: " + propName);
    }
    else {
      var objType = Object.prototype.toString.call(givenProp);
      if (objType == "[object UIAElementNil]") {
        throw new AssertionException("found no elements for " + given.toString() + '.' + propName + "()");
      }
      else if (objType == "[object Undefined]") {
        throw new AssertionException(given.toString() + '.' + propName + "() method not found.");
      }
    }

    // null indicates we don't care to match
    if (expectedProp === null) {
      continue;
    }

    var expectedPropType = typeof(expectedProp);
    if (expectedPropType == "string") {
      assertEquals(expectedProp, givenProp);
    }
    else if (expectedPropType == "number") {
      assertEquals(expectedProp, givenProp);
    }
    else if (expectedPropType == "boolean") {
      assertEquals(expectedProp, givenProp);
    }
    else if (expectedPropType == "function") {
      if (expectedProp.constructor == RegExp) {
        assertMatch(expectedProp, givenProp);
      }
      else {
        expectedProp(givenProp);
      }
    }
    else if (expectedPropType == "object") {
      if (expectedProp.constructor === Array) {
        assertEquals(expectedProp.length, givenProp.length, "Length of " + propName + " does not match");
        for (var i = 0; i < expectedProp.length; i++) {
          var exp = expectedProp[i];
          var giv = givenProp[i];
          assertPropertiesMatch(exp, giv, level + 1);
        }
      }
      else if (expectedProp.constructor === RegExp) {
        assertMatch(expectedProp, givenProp);
      }
      else if (typeof(givenProp) == "object") {
        assertPropertiesMatch(expectedProp, givenProp, level + 1);
      }
      else {
        var message = "[" + propName + "]: Unknown type of object constructor: " + expectedProp.constructor;
        UIALogger.logError(message);
        throw new AssertionException(message);
      }
    }
    else {
      UIALogger.logError("[" + propName + "]: unknown type for expectedProp: " + typeof(expectedProp));
    }
  }
}

}