// 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.FunctionMockTest');
goog.setTestOnly('goog.testing.FunctionMockTest');

goog.require('goog.array');
goog.require('goog.string');
goog.require('goog.testing');
goog.require('goog.testing.FunctionMock');
goog.require('goog.testing.Mock');
goog.require('goog.testing.StrictMock');
goog.require('goog.testing.asserts');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.mockmatchers');

// Global scope so we can tear it down safely
var mockGlobal;

function tearDown() {
  if (mockGlobal) {
    mockGlobal.$tearDown();
  }
}


//----- Tests for goog.testing.FunctionMock

function testMockFunctionCallOrdering() {
  var doOneTest = function(mockFunction, success, expected_args, actual_args) {
    goog.array.forEach(expected_args, function(arg) { mockFunction(arg); });
    mockFunction.$replay();
    var callFunction = function() {
      goog.array.forEach(actual_args, function(arg) { mockFunction(arg); });
      mockFunction.$verify();
    };
    if (success) {
      callFunction();
    } else {
      assertThrows(callFunction);
    }
  };

  var doTest = function(strict_ok, loose_ok, expected_args, actual_args) {
    doOneTest(goog.testing.createFunctionMock(), strict_ok,
              expected_args, actual_args);
    doOneTest(goog.testing.createFunctionMock('name'), strict_ok,
              expected_args, actual_args);
    doOneTest(goog.testing.createFunctionMock('name', goog.testing.Mock.STRICT),
              strict_ok, expected_args, actual_args);
    doOneTest(goog.testing.createFunctionMock('name', goog.testing.Mock.LOOSE),
              loose_ok, expected_args, actual_args);
  };

  doTest(true, true, [1, 2], [1, 2]);
  doTest(false, true, [1, 2], [2, 1]);
  doTest(false, false, [1, 2], [2, 2]);
  doTest(false, false, [1, 2], [1]);
  doTest(false, false, [1, 2], [1, 1]);
  doTest(false, false, [1, 2], [1]);
}

function testMocksFunctionWithNoArgs() {
  var mockFoo = goog.testing.createFunctionMock();
  mockFoo();
  mockFoo.$replay();
  mockFoo();
  mockFoo.$verify();
}

function testMocksFunctionWithOneArg() {
  var mockFoo = goog.testing.createFunctionMock();
  mockFoo('x');
  mockFoo.$replay();
  mockFoo('x');
  mockFoo.$verify();
}

function testMocksFunctionWithMultipleArgs() {
  var mockFoo = goog.testing.createFunctionMock();
  mockFoo('x', 'y');
  mockFoo.$replay();
  mockFoo('x', 'y');
  mockFoo.$verify();
}

function testFailsIfCalledWithIncorrectArgs() {
  var mockFoo = goog.testing.createFunctionMock();

  mockFoo();
  mockFoo.$replay();
  assertThrows(function() {mockFoo('x');});
  mockFoo.$reset();

  mockFoo('x');
  mockFoo.$replay();
  assertThrows(function() {mockFoo();});
  mockFoo.$reset();

  mockFoo('x');
  mockFoo.$replay();
  assertThrows(function() {mockFoo('x', 'y');});
  mockFoo.$reset();

  mockFoo('x', 'y');
  mockFoo.$replay();
  assertThrows(function() {mockFoo('x');});
  mockFoo.$reset();

  mockFoo('correct');
  mockFoo.$replay();
  assertThrows(function() {mockFoo('wrong');});
  mockFoo.$reset();

  mockFoo('correct', 'args');
  mockFoo.$replay();
  assertThrows(function() {mockFoo('wrong', 'args');});
  mockFoo.$reset();
}

function testMocksFunctionWithReturnValue() {
  var mockFoo = goog.testing.createFunctionMock();
  mockFoo().$returns('bar');
  mockFoo.$replay();
  assertEquals('bar', mockFoo());
  mockFoo.$verify();
}

function testFunctionMockWorksWhenPassedAsACallback() {
  var invoker = {
    register: function(callback) {
      this.callback = callback;
    },

    invoke: function(args) {
      return this.callback(args);
    }
  };

  var mockFunction = goog.testing.createFunctionMock();
  mockFunction('bar').$returns('baz');

  mockFunction.$replay();
  invoker.register(mockFunction);
  assertEquals('baz', invoker.invoke('bar'));
  mockFunction.$verify();
}

function testFunctionMockQuacksLikeAStrictMock() {
  var mockFunction = goog.testing.createFunctionMock();
  assertQuacksLike(mockFunction, goog.testing.StrictMock);
}


//----- Global functions for goog.testing.GlobalFunctionMock to mock

function globalFoo() {
  return 'I am Spartacus!';
}

function globalBar(who, what) {
  return [who, 'is', what].join(' ');
}


//----- Tests for goog.testing.createGlobalFunctionMock

function testMocksGlobalFunctionWithNoArgs() {
  mockGlobal = goog.testing.createGlobalFunctionMock('globalFoo');
  mockGlobal().$returns('No, I am Spartacus!');

  mockGlobal.$replay();
  assertEquals('No, I am Spartacus!', globalFoo());
  mockGlobal.$verify();
}

function testMocksGlobalFunctionUsingGlobalName() {
  goog.testing.createGlobalFunctionMock('globalFoo');
  globalFoo().$returns('No, I am Spartacus!');

  globalFoo.$replay();
  assertEquals('No, I am Spartacus!', globalFoo());
  globalFoo.$verify();
  globalFoo.$tearDown();
}

function testMocksGlobalFunctionWithArgs() {
  var mockReturnValue = 'Noam is Chomsky!';
  mockGlobal = goog.testing.createGlobalFunctionMock('globalBar');
  mockGlobal('Noam', 'Spartacus').$returns(mockReturnValue);

  mockGlobal.$replay();
  assertEquals(mockReturnValue, globalBar('Noam', 'Spartacus'));
  mockGlobal.$verify();
}

function testGlobalFunctionMockFailsWithIncorrectArgs() {
  mockGlobal = goog.testing.createGlobalFunctionMock('globalBar');
  mockGlobal('a', 'b');

  mockGlobal.$replay();

  assertThrows('Mock should have failed because of incorrect arguments',
      function() {globalBar('b', 'a')});
}

function testGlobalFunctionMockQuacksLikeAFunctionMock() {
  mockGlobal = goog.testing.createGlobalFunctionMock('globalFoo');
  assertQuacksLike(mockGlobal, goog.testing.FunctionMock);
}

function testMockedFunctionsAvailableInGlobalAndGoogGlobalAndWindowScope() {
  mockGlobal = goog.testing.createGlobalFunctionMock('globalFoo');

  // we expect this call 3 times through global, goog.global and window scope
  mockGlobal().$times(3);

  mockGlobal.$replay();
  goog.global.globalFoo();
  window.globalFoo();
  globalFoo();
  mockGlobal.$verify();
}

function testTearDownRestoresOriginalGlobalFunction() {
  mockGlobal = goog.testing.createGlobalFunctionMock('globalFoo');
  mockGlobal().$returns('No, I am Spartacus!');

  mockGlobal.$replay();
  assertEquals('No, I am Spartacus!', globalFoo());
  mockGlobal.$tearDown();
  assertEquals('I am Spartacus!', globalFoo());
  mockGlobal.$verify();
}

function testTearDownHandlesMultipleMocking() {
  var mock1 = goog.testing.createGlobalFunctionMock('globalFoo');
  var mock2 = goog.testing.createGlobalFunctionMock('globalFoo');
  var mock3 = goog.testing.createGlobalFunctionMock('globalFoo');
  mock1().$returns('No, I am Spartacus 1!');
  mock2().$returns('No, I am Spartacus 2!');
  mock3().$returns('No, I am Spartacus 3!');

  mock1.$replay();
  mock2.$replay();
  mock3.$replay();
  assertEquals('No, I am Spartacus 3!', globalFoo());
  mock3.$tearDown();
  assertEquals('No, I am Spartacus 2!', globalFoo());
  mock2.$tearDown();
  assertEquals('No, I am Spartacus 1!', globalFoo());
  mock1.$tearDown();
  assertEquals('I am Spartacus!', globalFoo());
}

function testGlobalFunctionMockCallOrdering() {
  var mock = goog.testing.createGlobalFunctionMock('globalFoo');
  mock(1);
  mock(2);
  mock.$replay();
  assertThrows(function() {globalFoo(2);});
  mock.$tearDown();

  mock = goog.testing.createGlobalFunctionMock('globalFoo',
                                               goog.testing.Mock.STRICT);
  mock(1);
  mock(2);
  mock.$replay();
  globalFoo(1);
  globalFoo(2);
  mock.$verify();
  mock.$tearDown();

  mock = goog.testing.createGlobalFunctionMock('globalFoo',
                                               goog.testing.Mock.STRICT);
  mock(1);
  mock(2);
  mock.$replay();
  assertThrows(function() {globalFoo(2);});
  mock.$tearDown();

  mock = goog.testing.createGlobalFunctionMock('globalFoo',
                                               goog.testing.Mock.LOOSE);
  mock(1);
  mock(2);
  mock.$replay();
  globalFoo(2);
  globalFoo(1);
  mock.$verify();
  mock.$tearDown();
}

//----- Functions for goog.testing.MethodMock to mock

var mynamespace = {};

mynamespace.myMethod = function() {
  return 'I should be mocked.';
};

function testMocksMethod() {
  mockMethod = goog.testing.createMethodMock(mynamespace, 'myMethod');
  mockMethod().$returns('I have been mocked!');

  mockMethod.$replay();
  assertEquals('I have been mocked!', mockMethod());
  mockMethod.$verify();
}

function testMocksMethodInNamespace() {
  goog.testing.createMethodMock(mynamespace, 'myMethod');
  mynamespace.myMethod().$returns('I have been mocked!');

  mynamespace.myMethod.$replay();
  assertEquals('I have been mocked!', mynamespace.myMethod());
  mynamespace.myMethod.$verify();
  mynamespace.myMethod.$tearDown();
}

function testMethodMockCanOnlyMockExistingMethods() {
  assertThrows(function() {
    goog.testing.createMethodMock(mynamespace, 'doesNotExist');
  });
}

function testMethodMockCallOrdering() {
  goog.testing.createMethodMock(mynamespace, 'myMethod');
  mynamespace.myMethod(1);
  mynamespace.myMethod(2);
  mynamespace.myMethod.$replay();
  assertThrows(function() {mynamespace.myMethod(2);});
  mynamespace.myMethod.$tearDown();

  goog.testing.createMethodMock(mynamespace, 'myMethod',
                                goog.testing.Mock.STRICT);
  mynamespace.myMethod(1);
  mynamespace.myMethod(2);
  mynamespace.myMethod.$replay();
  mynamespace.myMethod(1);
  mynamespace.myMethod(2);
  mynamespace.myMethod.$verify();
  mynamespace.myMethod.$tearDown();

  goog.testing.createMethodMock(mynamespace, 'myMethod',
                                goog.testing.Mock.STRICT);
  mynamespace.myMethod(1);
  mynamespace.myMethod(2);
  mynamespace.myMethod.$replay();
  assertThrows(function() {mynamespace.myMethod(2);});
  mynamespace.myMethod.$tearDown();

  goog.testing.createMethodMock(mynamespace, 'myMethod',
                                goog.testing.Mock.LOOSE);
  mynamespace.myMethod(1);
  mynamespace.myMethod(2);
  mynamespace.myMethod.$replay();
  mynamespace.myMethod(2);
  mynamespace.myMethod(1);
  mynamespace.myMethod.$verify();
  mynamespace.myMethod.$tearDown();
}

//----- Functions for goog.testing.createConstructorMock to mock

var constructornamespace = {};

constructornamespace.MyConstructor = function() {
};

constructornamespace.MyConstructor.prototype.myMethod = function() {
  return 'I should be mocked.';
};

constructornamespace.MyConstructorWithArgument = function(argument) {
  this.argument_ = argument;
};

constructornamespace.MyConstructorWithArgument.prototype.myMethod = function() {
  return this.argument_;
};

constructornamespace.MyConstructorWithClassMembers = function() {
};

constructornamespace.MyConstructorWithClassMembers.CONSTANT = 42;

constructornamespace.MyConstructorWithClassMembers.classMethod = function() {
  return 'class method return value';
};

function testConstructorMock() {
  var mockObject =
      new goog.testing.StrictMock(constructornamespace.MyConstructor);
  var mockConstructor = goog.testing.createConstructorMock(
      constructornamespace, 'MyConstructor');
  mockConstructor().$returns(mockObject);
  mockObject.myMethod().$returns('I have been mocked!');

  mockConstructor.$replay();
  mockObject.$replay();
  assertEquals('I have been mocked!',
      new constructornamespace.MyConstructor().myMethod());
  mockConstructor.$verify();
  mockObject.$verify();
  mockConstructor.$tearDown();
}

function testConstructorMockWithArgument() {
  var mockObject = new goog.testing.StrictMock(
      constructornamespace.MyConstructorWithArgument);
  var mockConstructor = goog.testing.createConstructorMock(
      constructornamespace, 'MyConstructorWithArgument');
  mockConstructor(goog.testing.mockmatchers.isString).$returns(mockObject);
  mockObject.myMethod().$returns('I have been mocked!');

  mockConstructor.$replay();
  mockObject.$replay();
  assertEquals('I have been mocked!',
      new constructornamespace.MyConstructorWithArgument('I should be mocked.')
          .myMethod());
  mockConstructor.$verify();
  mockObject.$verify();
  mockConstructor.$tearDown();
}


/**
 * Test that class members are copied to the mock constructor.
 */
function testConstructorMockWithClassMembers() {
  var mockConstructor = goog.testing.createConstructorMock(
      constructornamespace, 'MyConstructorWithClassMembers');
  assertEquals(42, constructornamespace.MyConstructorWithClassMembers.CONSTANT);
  assertEquals('class method return value',
      constructornamespace.MyConstructorWithClassMembers.classMethod());
  mockConstructor.$tearDown();
}

function testConstructorMockCallOrdering() {
  var instance = {};

  goog.testing.createConstructorMock(constructornamespace,
                                     'MyConstructorWithArgument');
  constructornamespace.MyConstructorWithArgument(1).$returns(instance);
  constructornamespace.MyConstructorWithArgument(2).$returns(instance);
  constructornamespace.MyConstructorWithArgument.$replay();
  assertThrows(
      function() {new constructornamespace.MyConstructorWithArgument(2);});
  constructornamespace.MyConstructorWithArgument.$tearDown();

  goog.testing.createConstructorMock(constructornamespace,
                                     'MyConstructorWithArgument',
                                     goog.testing.Mock.STRICT);
  constructornamespace.MyConstructorWithArgument(1).$returns(instance);
  constructornamespace.MyConstructorWithArgument(2).$returns(instance);
  constructornamespace.MyConstructorWithArgument.$replay();
  new constructornamespace.MyConstructorWithArgument(1);
  new constructornamespace.MyConstructorWithArgument(2);
  constructornamespace.MyConstructorWithArgument.$verify();
  constructornamespace.MyConstructorWithArgument.$tearDown();

  goog.testing.createConstructorMock(constructornamespace,
                                     'MyConstructorWithArgument',
                                     goog.testing.Mock.STRICT);
  constructornamespace.MyConstructorWithArgument(1).$returns(instance);
  constructornamespace.MyConstructorWithArgument(2).$returns(instance);
  constructornamespace.MyConstructorWithArgument.$replay();
  assertThrows(
      function() {new constructornamespace.MyConstructorWithArgument(2);});
  constructornamespace.MyConstructorWithArgument.$tearDown();

  goog.testing.createConstructorMock(constructornamespace,
                                     'MyConstructorWithArgument',
                                     goog.testing.Mock.LOOSE);
  constructornamespace.MyConstructorWithArgument(1).$returns(instance);
  constructornamespace.MyConstructorWithArgument(2).$returns(instance);
  constructornamespace.MyConstructorWithArgument.$replay();
  new constructornamespace.MyConstructorWithArgument(2);
  new constructornamespace.MyConstructorWithArgument(1);
  constructornamespace.MyConstructorWithArgument.$verify();
  constructornamespace.MyConstructorWithArgument.$tearDown();
}

//----- Helper assertions

function assertQuacksLike(obj, target) {
  for (meth in target.prototype) {
    if (!goog.string.endsWith(meth, '_')) {
      assertNotUndefined('Should have implemented ' + meth + '()', obj[meth]);
    }
  }
}
