// Copyright 2008 The Closure Library Authors. All Rights Reserved.
//
// Licensed 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.

goog.provide('goog.testing.mockmatchersTest');
goog.setTestOnly('goog.testing.mockmatchersTest');

goog.require('goog.dom');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.mockmatchers');
goog.require('goog.testing.mockmatchers.ArgumentMatcher');

// A local reference to the mockmatchers namespace.
var matchers = goog.testing.mockmatchers;

// Simple classes to test the InstanceOf matcher.
var foo = function() {};
var bar = function() {};

// Simple class to test adding error messages to
// MockExpectation objects
function MockMock() {
  this.errorMessages = [];
}

var mockExpect = null;

MockMock.prototype.addErrorMessage = function(msg) {
  this.errorMessages.push(msg);
};


MockMock.prototype.getErrorMessageCount = function() {
  return this.errorMessages.length;
};


function setUp() {
  mockExpect = new MockMock();
}


function testNoMatchName() {
  // A matcher that does not fill in the match name
  var matcher = new goog.testing.mockmatchers.ArgumentMatcher(goog.isString);

  // Make sure the lack of match name doesn't affect the ability
  // to return True/False
  assertTrue(matcher.matches('hello'));
  assertFalse(matcher.matches(123));

  // Make sure we handle the lack of a match name
  assertFalse(matcher.matches(456, mockExpect));
  assertEquals(1, mockExpect.errorMessages.length);
  assertEquals('Expected: missing mockmatcher description ' +
      'but was: <456> (Number)', mockExpect.errorMessages[0]);
}


function testInstanceOf() {
  var matcher = new matchers.InstanceOf(foo);
  assertTrue(matcher.matches(new foo()));
  assertFalse(matcher.matches(new bar()));

  assertFalse(matcher.matches(new bar(), mockExpect));
  assertEquals(1, mockExpect.errorMessages.length);
  assertEquals('Expected: instanceOf() ' +
      'but was: <[object Object]> (Object)',
      mockExpect.errorMessages[0]);
}


function testTypeOf() {
  var matcher = new matchers.TypeOf('number');
  assertTrue(matcher.matches(1));
  assertTrue(matcher.matches(2));
  assertFalse(matcher.matches('test'));

  assertFalse(matcher.matches(true, mockExpect));
  assertEquals(1, mockExpect.errorMessages.length);
  assertEquals('Expected: typeOf(number) but was: <true> (Boolean)',
      mockExpect.errorMessages[0]);
}


function testRegexpMatch() {
  var matcher = new matchers.RegexpMatch(/^cho[dtp]/);
  assertTrue(matcher.matches('chodhop'));
  assertTrue(matcher.matches('chopper'));
  assertFalse(matcher.matches('chocolate'));
  assertFalse(matcher.matches(null));

  assertFalse(matcher.matches('an anger', mockExpect));
  assertEquals(1, mockExpect.errorMessages.length);
  assertEquals('Expected: match(/^cho[dtp]/) but was: <an anger> (String)',
      mockExpect.errorMessages[0]);
}


function testObjectEquals() {
  // Test a simple match.
  var simpleMatcher = new matchers.ObjectEquals({name: 'Bob', age: 42});
  assertTrue(simpleMatcher.matches({name: 'Bob', age: 42}, mockExpect));
  assertEquals(0, mockExpect.getErrorMessageCount());
  expectObjectEqualsFailure(simpleMatcher, {name: 'Bill', age: 42},
      'name: Expected <Bob> (String) but was <Bill> (String)');
  expectObjectEqualsFailure(simpleMatcher, {name: 'Bob', age: 21},
      'age: Expected <42> (Number) but was <21> (Number)');
  expectObjectEqualsFailure(simpleMatcher, {name: 'Bob'},
      'property age not present in actual Object');
  expectObjectEqualsFailure(simpleMatcher,
      {name: 'Bob', age: 42, country: 'USA'},
      'property country not present in expected Object');

  // Multiple mismatches should include multiple messages.
  expectObjectEqualsFailure(simpleMatcher, {name: 'Jim', age: 36},
      'name: Expected <Bob> (String) but was <Jim> (String)\n' +
      '   age: Expected <42> (Number) but was <36> (Number)');
}

function testComplexObjectEquals() {
  var complexMatcher = new matchers.ObjectEquals(
      {a: 'foo', b: 2, c: ['bar', 3], d: {sub1: 'baz', sub2: -1}});
  assertTrue(complexMatcher.matches(
      {a: 'foo', b: 2, c: ['bar', 3], d: {sub1: 'baz', sub2: -1}}));
  expectObjectEqualsFailure(complexMatcher,
      {a: 'foo', b: 2, c: ['bar', 3], d: {sub1: 'zap', sub2: -1}},
      'sub1: Expected <baz> (String) but was <zap> (String)');
  expectObjectEqualsFailure(complexMatcher,
      {a: 'foo', b: 2, c: ['bar', 6], d: {sub1: 'baz', sub2: -1}},
      'c[1]: Expected <3> (Number) but was <6> (Number)');
}


function testSaveArgument() {
  var saveMatcher = new matchers.SaveArgument();
  assertTrue(saveMatcher.matches(42));
  assertEquals(42, saveMatcher.arg);

  saveMatcher = new matchers.SaveArgument(goog.isString);
  assertTrue(saveMatcher.matches('test'));
  assertEquals('test', saveMatcher.arg);
  assertFalse(saveMatcher.matches(17));
  assertEquals(17, saveMatcher.arg);

  saveMatcher = new matchers.SaveArgument(new matchers.ObjectEquals({
    value: 'value'
  }));
  assertTrue(saveMatcher.matches({ value: 'value' }));
  assertEquals('value', saveMatcher.arg.value);
  assertFalse(saveMatcher.matches('test'));
  assertEquals('test', saveMatcher.arg);
}


function testIsArray() {
  assertTrue(matchers.isArray.matches([]));
  assertTrue(matchers.isArray.matches(new Array()));
  assertFalse(matchers.isArray.matches('test'));

  assertFalse(matchers.isArray.matches({}, mockExpect));
  assertEquals(1, mockExpect.errorMessages.length);
  assertEquals('Expected: isArray but was: <[object Object]> (Object)',
      mockExpect.errorMessages[0]);
}


function testIsArrayLike() {
  var nodeList = (function() {
    var div = document.createElement('div');
    div.appendChild(document.createElement('p'));
    div.appendChild(document.createElement('p'));
    return div.getElementsByTagName('div');
  })();

  assertTrue(matchers.isArrayLike.matches([]));
  assertTrue(matchers.isArrayLike.matches(new Array()));
  assertTrue(matchers.isArrayLike.matches(nodeList));
  assertFalse(matchers.isArrayLike.matches('test'));

  assertFalse(matchers.isArrayLike.matches(3, mockExpect));
  assertEquals(1, mockExpect.errorMessages.length);
  assertEquals('Expected: isArrayLike but was: <3> (Number)',
      mockExpect.errorMessages[0]);
}


function testIsDateLike() {
  assertTrue(matchers.isDateLike.matches(new Date()));
  assertFalse(matchers.isDateLike.matches('test'));

  assertFalse(matchers.isDateLike.matches('test', mockExpect));
  assertEquals(1, mockExpect.errorMessages.length);
  assertEquals('Expected: isDateLike but was: <test> (String)',
      mockExpect.errorMessages[0]);
}


function testIsString() {
  assertTrue(matchers.isString.matches('a'));
  assertTrue(matchers.isString.matches('b'));
  assertFalse(matchers.isString.matches(null));

  assertFalse(matchers.isString.matches(null, mockExpect));
  assertEquals(1, mockExpect.errorMessages.length);
  assertEquals('Expected: isString but was: <null>',
      mockExpect.errorMessages[0]);
}


function testIsBoolean() {
  assertTrue(matchers.isBoolean.matches(true));
  assertTrue(matchers.isBoolean.matches(false));
  assertFalse(matchers.isBoolean.matches(null));

  assertFalse(matchers.isBoolean.matches([], mockExpect));
  assertEquals(1, mockExpect.errorMessages.length);
  assertEquals('Expected: isBoolean but was: <> (Array)',
      mockExpect.errorMessages[0]);
}


function testIsNumber() {
  assertTrue(matchers.isNumber.matches(-1));
  assertTrue(matchers.isNumber.matches(1));
  assertTrue(matchers.isNumber.matches(1.25));
  assertFalse(matchers.isNumber.matches(null));

  assertFalse(matchers.isNumber.matches('hello', mockExpect));
  assertEquals(1, mockExpect.errorMessages.length);
  assertEquals('Expected: isNumber but was: <hello> (String)',
      mockExpect.errorMessages[0]);
}


function testIsFunction() {
  assertTrue(matchers.isFunction.matches(function() {}));
  assertFalse(matchers.isFunction.matches('test'));

  assertFalse(matchers.isFunction.matches({}, mockExpect));
  assertEquals(1, mockExpect.errorMessages.length);
  assertEquals('Expected: isFunction but was: <[object Object]> (Object)',
      mockExpect.errorMessages[0]);
}


function testIsObject() {
  assertTrue(matchers.isObject.matches({}));
  assertTrue(matchers.isObject.matches(new Object()));
  assertTrue(matchers.isObject.matches(new function() {}));
  assertTrue(matchers.isObject.matches([]));
  assertTrue(matchers.isObject.matches(new Array()));
  assertTrue(matchers.isObject.matches(function() {}));
  assertFalse(matchers.isObject.matches(null));

  assertFalse(matchers.isObject.matches(1234, mockExpect));
  assertEquals(1, mockExpect.errorMessages.length);
  assertEquals('Expected: isObject but was: <1234> (Number)',
      mockExpect.errorMessages[0]);
}


function testIsNodeLike() {
  assertFalse(matchers.isNodeLike.matches({}));
  assertFalse(matchers.isNodeLike.matches(1));
  assertFalse(matchers.isNodeLike.matches(function() {}));
  assertFalse(matchers.isNodeLike.matches(false));
  assertTrue(matchers.isNodeLike.matches(document.body));
  assertTrue(matchers.isNodeLike.matches(goog.dom.getElement('someDiv')));

  assertFalse(matchers.isNodeLike.matches('test', mockExpect));
  assertEquals(1, mockExpect.errorMessages.length);
  assertEquals('Expected: isNodeLike but was: <test> (String)',
      mockExpect.errorMessages[0]);
}


function testIgnoreArgumentsMatcher() {
  // ignoreArgument always returns true:
  assertTrue(matchers.ignoreArgument.matches());
  assertTrue(matchers.ignoreArgument.matches(356));
  assertTrue(matchers.ignoreArgument.matches('str'));
  assertTrue(matchers.ignoreArgument.matches(['array', 123, false]));
  assertTrue(matchers.ignoreArgument.matches({'map': 1, key2: 'value2'}));
}


function testFlexibleArrayMatcher() {
  // Test that basic lists are verified properly.
  var a1 = [1, 'test'];
  var a2 = [1, 'test'];
  var a3 = [1, 'test', 'extra'];
  assertTrue(matchers.flexibleArrayMatcher(a1, a2));
  assertFalse(matchers.flexibleArrayMatcher(a1, a3));

  // Test that basic lists with basic class instances are verified properly.
  var instance = new foo();
  a1 = [1, 'test', instance];
  a2 = [1, 'test', instance];
  a3 = [1, 'test', new foo()];
  assertTrue(matchers.flexibleArrayMatcher(a1, a2));
  assertTrue(matchers.flexibleArrayMatcher(a1, a3));

  // Create an argument verifier that returns a consistent value.
  var verifyValue = true;
  var argVerifier = function() {};
  goog.inherits(argVerifier, matchers.ArgumentMatcher);
  argVerifier.prototype.matches = function(arg) {
    return verifyValue;
  };

  // Test that the arguments are always verified when the verifier returns
  // true.
  a1 = [1, 'test', new argVerifier()];
  a2 = [1, 'test', 'anything'];
  a3 = [1, 'test', 12345];
  assertTrue(matchers.flexibleArrayMatcher(a1, a2));
  assertTrue(matchers.flexibleArrayMatcher(a1, a3));

  // Now test the case when then verifier returns false.
  verifyValue = false;
  assertFalse(matchers.flexibleArrayMatcher(a1, a2));
  assertFalse(matchers.flexibleArrayMatcher(a1, a3));

  // And test we report errors back up via the opt_expectation
  assertFalse(matchers.flexibleArrayMatcher(a2, a3, mockExpect));
  assertEquals(1, mockExpect.errorMessages.length);
  assertEquals(
      'Expected <anything> (String) but was <12345> (Number)\n' +
      '    Expected <anything> (String) but was <12345> (Number)',
      mockExpect.errorMessages[0]);

  // And test we report errors found via the matcher
  a1 = [1, goog.testing.mockmatchers.isString];
  a2 = [1, 'test string'];
  a3 = [1, null];
  assertTrue(matchers.flexibleArrayMatcher(a1, a2, mockExpect));
  assertFalse(matchers.flexibleArrayMatcher(a1, a3, mockExpect));
  // Old error is still there
  assertEquals(2, mockExpect.errorMessages.length);
  assertEquals(
      'Expected <anything> (String) but was <12345> (Number)\n' +
      '    Expected <anything> (String) but was <12345> (Number)',
      mockExpect.errorMessages[0]);
  // plus the new error...
  assertEquals('Expected: isString but was: <null>',
      mockExpect.errorMessages[1]);
}


/**
 * Utility method for checking for an ObjectEquals match failure.  Checks that
 * the expected error message was included in the error messages appended to
 * the expectation object.
 * @param {goog.testing.mockmatchers.ArgumentMatcher.ObjectEquals} matcher
 *     The matcher to test against.
 * @param {Object} matchObject The object to compare.
 * @param {string=} opt_errorMsg The deep object comparison failure message
 *     to check for.
 */
function expectObjectEqualsFailure(matcher, matchObject, opt_errorMsg) {
  mockExpect.errorMessages = [];
  assertFalse(matcher.matches(matchObject, mockExpect));
  assertNotEquals(0, mockExpect.getErrorMessageCount());
  if (opt_errorMsg) {
    assertContains(opt_errorMsg, mockExpect.errorMessages[0]);
  }
}
