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

goog.require('goog.testing.LooseMock');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.mockmatchers');

// The object that we will be mocking
var RealObject = function() {
};

RealObject.prototype.a = function() {
  fail('real object should never be called');
};

RealObject.prototype.b = function() {
  fail('real object should never be called');
};

var mock;

var stubs;

function setUp() {
  var obj = new RealObject();
  mock = new goog.testing.LooseMock(obj);
  stubs = new goog.testing.PropertyReplacer();
}

function tearDown() {
  stubs.reset();
}

/*
 * Calling this method evades the logTestFailure in
 * goog.testing.Mock.prototype.$recordAndThrow, so it doesn't look like the
 * test failed.
 */
function silenceFailureLogging() {
  if (goog.global['G_testRunner']) {
    stubs.set(goog.global['G_testRunner'],
        'logTestFailure', goog.nullFunction);
  }
}

function unsilenceFailureLogging() {
  stubs.reset();
}


/**
 * Version of assertThrows that doesn't log the exception generated by the
 * mocks under test.
 * TODO: would be nice to check that a particular substring is in the thrown
 * message so we know it's not something dumb like a syntax error
 */
function assertThrowsQuiet(var_args) {
  silenceFailureLogging();
  assertThrows.apply(null, arguments);
  unsilenceFailureLogging();
}

// Most of the basic functionality is tested in strictmock_test; these tests
// cover the cases where loose mocks are different from strict mocks
function testSimpleExpectations() {
  mock.a(5);
  mock.b();
  mock.$replay();
  mock.a(5);
  mock.b();
  mock.$verify();

  mock.$reset();

  mock.a();
  mock.b();
  mock.$replay();
  mock.b();
  mock.a();
  mock.$verify();

  mock.$reset();

  mock.a(5).$times(2);
  mock.a(5);
  mock.a(2);
  mock.$replay();
  mock.a(5);
  mock.a(5);
  mock.a(5);
  mock.a(2);
  mock.$verify();
}


function testMultipleExpectations() {
  mock.a().$returns(1);
  mock.a().$returns(2);
  mock.$replay();
  assertEquals(1, mock.a());
  assertEquals(2, mock.a());
  mock.$verify();
}


function testMultipleExpectationArgs() {
  mock.a('asdf').$anyTimes();
  mock.a('qwer').$anyTimes();
  mock.b().$times(3);
  mock.$replay();
  mock.a('asdf');
  mock.b();
  mock.a('asdf');
  mock.a('qwer');
  mock.b();
  mock.a('qwer');
  mock.b();
  mock.$verify();

  mock.$reset();

  mock.a('asdf').$anyTimes();
  mock.a('qwer').$anyTimes();
  mock.$replay();
  mock.a('asdf');
  mock.a('qwer');
  goog.bind(mock.a, mock, 'asdf');
  goog.bind(mock.$verify, mock);
}

function testSameMethodOutOfOrder() {
  mock.a('foo').$returns(1);
  mock.a('bar').$returns(2);
  mock.$replay();
  assertEquals(2, mock.a('bar'));
  assertEquals(1, mock.a('foo'));
}

function testSameMethodDifferentReturnValues() {
  mock.a('foo').$returns(1).$times(2);
  mock.a('foo').$returns(3);
  mock.a('bar').$returns(2);
  mock.$replay();
  assertEquals(1, mock.a('foo'));
  assertEquals(2, mock.a('bar'));
  assertEquals(1, mock.a('foo'));
  assertEquals(3, mock.a('foo'));
  assertThrowsQuiet(function() {
    mock.a('foo');
    mock.$verify();
  });
}

function testSameMethodBrokenExpectations() {
  // This is a weird corner case.
  // No way to ever make this verify no matter what you call after replaying,
  // because the second expectation of mock.a('foo') will be masked by
  // the first expectation that can be called any number of times, and so we
  // can never satisfy that second expectation.
  mock.a('foo').$returns(1).$anyTimes();
  mock.a('bar').$returns(2);
  mock.a('foo').$returns(3);

  // LooseMock can detect this case and fail on $replay.
  assertThrowsQuiet(goog.bind(mock.$replay, mock));
  mock.$reset();

  // This is a variant of the corner case above, but it's harder to determine
  // that the expectation to mock.a('bar') can never be satisfied. So we don't
  // fail on $replay, but we do fail on $verify.
  mock.a(goog.testing.mockmatchers.isString).$returns(1).$anyTimes();
  mock.a('bar').$returns(2);
  mock.$replay();

  assertEquals(1, mock.a('foo'));
  assertEquals(1, mock.a('bar'));
  assertThrowsQuiet(goog.bind(mock.$verify, mock));
}

function testSameMethodMultipleAnyTimes() {
  mock.a('foo').$returns(1).$anyTimes();
  mock.a('foo').$returns(2).$anyTimes();
  mock.$replay();
  assertEquals(1, mock.a('foo'));
  assertEquals(1, mock.a('foo'));
  assertEquals(1, mock.a('foo'));
  // Note we'll never return 2 but that's ok.
  mock.$verify();
}

function testFailingFast() {
  mock.a().$anyTimes();
  mock.$replay();
  mock.a();
  mock.a();
  assertThrowsQuiet(goog.bind(mock.b, mock));
  mock.$reset();

  // too many
  mock.a();
  mock.b();
  mock.$replay();
  mock.a();
  mock.b();

  var message;
  silenceFailureLogging();
  try {
    mock.a();
  } catch (e) {
    message = e.message;
  }
  unsilenceFailureLogging();

  assertTrue('No exception thrown on unexpected call', goog.isDef(message));
  assertContains('Too many calls to a', message);
}

function testTimes() {
  mock.a().$times(3);
  mock.b().$times(2);
  mock.$replay();
  mock.a();
  mock.b();
  mock.b();
  mock.a();
  mock.a();
  mock.$verify();
}


function testFailingSlow() {
  // not enough
  mock.a().$times(3);
  mock.$replay();
  mock.a();
  mock.a();
  assertThrowsQuiet(goog.bind(mock.$verify, mock));

  mock.$reset();

  // not enough, interleaved order
  mock.a().$times(3);
  mock.b().$times(3);
  mock.$replay();
  mock.a();
  mock.b();
  mock.a();
  mock.b();
  assertThrowsQuiet(goog.bind(mock.$verify, mock));

  mock.$reset();
  // bad args
  mock.a('asdf').$anyTimes();
  mock.$replay();
  mock.a('asdf');
  assertThrowsQuiet(goog.bind(mock.a, mock, 'qwert'));
  assertThrowsQuiet(goog.bind(mock.$verify, mock));
}


function testArgsAndReturns() {
  mock.a('asdf').$atLeastOnce().$returns(5);
  mock.b('qwer').$times(2).$returns(3);
  mock.$replay();
  assertEquals(5, mock.a('asdf'));
  assertEquals(3, mock.b('qwer'));
  assertEquals(5, mock.a('asdf'));
  assertEquals(5, mock.a('asdf'));
  assertEquals(3, mock.b('qwer'));
  mock.$verify();
}


function testThrows() {
  mock.a().$throws('exception!');
  mock.$replay();
  assertThrowsQuiet(goog.bind(mock.a, mock));
  mock.$verify();
}


function testDoes() {
  mock.a(1, 2).$does(function(a, b) {return a + b;});
  mock.$replay();
  assertEquals('Mock should call the function', 3, mock.a(1, 2));
  mock.$verify();
}

function testIgnoresExtraCalls() {
  mock = new goog.testing.LooseMock(RealObject, true);
  mock.a();
  mock.$replay();
  mock.a();
  mock.b(); // doesn't throw
  mock.$verify();
}

function testSkipAnyTimes() {
  mock = new goog.testing.LooseMock(RealObject);
  mock.a(1).$anyTimes();
  mock.a(2).$anyTimes();
  mock.a(3).$anyTimes();
  mock.$replay();
  mock.a(1);
  mock.a(3);
  mock.$verify();
}

function testErrorMessageForBadArgs() {
  mock.a();
  mock.$anyTimes();

  mock.$replay();

  var message;
  silenceFailureLogging();
  try {
    mock.a('a');
  } catch (e) {
    message = e.message;
  }
  unsilenceFailureLogging();

  assertTrue('No exception thrown on verify', goog.isDef(message));
  assertContains('Bad arguments to a()', message);
}
