// 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.

/**
 * @fileoverview Unit tests for goog.functions.
 */

goog.provide('goog.functionsTest');
goog.setTestOnly('goog.functionsTest');

goog.require('goog.array');
goog.require('goog.functions');
goog.require('goog.testing.MockClock');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.recordFunction');


var fTrue = makeCallOrderLogger('fTrue', true);
var gFalse = makeCallOrderLogger('gFalse', false);
var hTrue = makeCallOrderLogger('hTrue', true);

var stubs = new goog.testing.PropertyReplacer();

function setUp() {
  callOrder = [];
}

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

var foo = 'global';
var obj = {foo: 'obj'};

function getFoo(arg1, arg2) {
  return {foo: this.foo, arg1: arg1, arg2: arg2};
}

function testTrue() {
  assertTrue(goog.functions.TRUE());
}

function testFalse() {
  assertFalse(goog.functions.FALSE());
}

function testLock() {
  function add(var_args) {
    var result = 0;
    for (var i = 0; i < arguments.length; i++) {
      result += arguments[i];
    }
    return result;
  }

  assertEquals(6, add(1, 2, 3));
  assertEquals(0, goog.functions.lock(add)(1, 2, 3));
  assertEquals(3, goog.functions.lock(add, 2)(1, 2, 3));
  assertEquals(6, goog.partial(add, 1, 2)(3));
  assertEquals(3, goog.functions.lock(goog.partial(add, 1, 2))(3));
}

function testNth() {
  assertEquals(1, goog.functions.nth(0)(1));
  assertEquals(2, goog.functions.nth(1)(1, 2));
  assertEquals('a', goog.functions.nth(0)('a', 'b'));
  assertEquals(undefined, goog.functions.nth(0)());
  assertEquals(undefined, goog.functions.nth(1)(true));
  assertEquals(undefined, goog.functions.nth(-1)());
}

function testPartialRight() {
  var f = function(x, y) { return x / y; };
  var g = goog.functions.partialRight(f, 2);
  assertEquals(2, g(4));

  var h = goog.functions.partialRight(f, 4, 2);
  assertEquals(2, h());

  var i = goog.functions.partialRight(f);
  assertEquals(2, i(4, 2));
}

function testPartialRightUsesGlobal() {
  var f = function(x, y) {
    assertEquals(goog.global, this);
    return x / y;
  };
  var g = goog.functions.partialRight(f, 2);
  var h = goog.functions.partialRight(g, 4);
  assertEquals(2, h());
}

function testPartialRightWithCall() {
  var obj = {};
  var f = function(x, y) {
    assertEquals(obj, this);
    return x / y;
  };
  var g = goog.functions.partialRight(f, 2);
  var h = goog.functions.partialRight(g, 4);
  assertEquals(2, h.call(obj));
}

function testPartialRightAndBind() {
  // This ensures that this "survives" through a partialRight.
  var p = goog.functions.partialRight(getFoo, 'dog');
  var b = goog.bind(p, obj, 'hot');

  var res = b();
  assertEquals(obj.foo, res.foo);
  assertEquals('hot', res.arg1);
  assertEquals('dog', res.arg2);
}

function testBindAndPartialRight() {
  // This ensures that this "survives" through a partialRight.
  var b = goog.bind(getFoo, obj, 'hot');
  var p = goog.functions.partialRight(b, 'dog');

  var res = p();
  assertEquals(obj.foo, res.foo);
  assertEquals('hot', res.arg1);
  assertEquals('dog', res.arg2);
}

function testPartialRightMultipleCalls() {
  var f = goog.testing.recordFunction();

  var a = goog.functions.partialRight(f, 'foo');
  var b = goog.functions.partialRight(a, 'bar');

  a();
  a();
  b();
  b();

  assertEquals(4, f.getCallCount());

  var calls = f.getCalls();
  assertArrayEquals(['foo'], calls[0].getArguments());
  assertArrayEquals(['foo'], calls[1].getArguments());
  assertArrayEquals(['bar', 'foo'], calls[2].getArguments());
  assertArrayEquals(['bar', 'foo'], calls[3].getArguments());
}

function testIdentity() {
  assertEquals(3, goog.functions.identity(3));
  assertEquals(3, goog.functions.identity(3, 4, 5, 6));
  assertEquals('Hi there', goog.functions.identity('Hi there'));
  assertEquals(null, goog.functions.identity(null));
  assertEquals(undefined, goog.functions.identity());

  var arr = [1, 'b', null];
  assertEquals(arr, goog.functions.identity(arr));
  var obj = {a: 'ay', b: 'bee', c: 'see'};
  assertEquals(obj, goog.functions.identity(obj));
}

function testConstant() {
  assertEquals(3, goog.functions.constant(3)());
  assertEquals(undefined, goog.functions.constant()());
}

function testError() {
  var f = goog.functions.error('x');
  var e = assertThrows(
      'A function created by goog.functions.error must throw an error', f);
  assertEquals('x', e.message);
}

function testFail() {
  var obj = {};
  var f = goog.functions.fail(obj);
  var e = assertThrows(
      'A function created by goog.functions.raise must throw its input', f);
  assertEquals(obj, e);
}

function testCompose() {
  var add2 = function(x) { return x + 2; };

  var doubleValue = function(x) { return x * 2; };

  assertEquals(6, goog.functions.compose(doubleValue, add2)(1));
  assertEquals(4, goog.functions.compose(add2, doubleValue)(1));
  assertEquals(6, goog.functions.compose(add2, add2, doubleValue)(1));
  assertEquals(
      12, goog.functions.compose(doubleValue, add2, add2, doubleValue)(1));
  assertUndefined(goog.functions.compose()(1));
  assertEquals(3, goog.functions.compose(add2)(1));

  var add2Numbers = function(x, y) { return x + y; };
  assertEquals(17, goog.functions.compose(add2Numbers)(10, 7));
  assertEquals(34, goog.functions.compose(doubleValue, add2Numbers)(10, 7));
}

function testAdd() {
  assertUndefined(goog.functions.sequence()());
  assertCallOrderAndReset([]);

  assert(goog.functions.sequence(fTrue)());
  assertCallOrderAndReset(['fTrue']);

  assertFalse(goog.functions.sequence(fTrue, gFalse)());
  assertCallOrderAndReset(['fTrue', 'gFalse']);

  assert(goog.functions.sequence(fTrue, gFalse, hTrue)());
  assertCallOrderAndReset(['fTrue', 'gFalse', 'hTrue']);

  assert(goog.functions.sequence(goog.functions.identity)(true));
  assertFalse(goog.functions.sequence(goog.functions.identity)(false));
}

function testAnd() {
  // the return value is unspecified for an empty and
  goog.functions.and()();
  assertCallOrderAndReset([]);

  assert(goog.functions.and(fTrue)());
  assertCallOrderAndReset(['fTrue']);

  assertFalse(goog.functions.and(fTrue, gFalse)());
  assertCallOrderAndReset(['fTrue', 'gFalse']);

  assertFalse(goog.functions.and(fTrue, gFalse, hTrue)());
  assertCallOrderAndReset(['fTrue', 'gFalse']);

  assert(goog.functions.and(goog.functions.identity)(true));
  assertFalse(goog.functions.and(goog.functions.identity)(false));
}

function testOr() {
  // the return value is unspecified for an empty or
  goog.functions.or()();
  assertCallOrderAndReset([]);

  assert(goog.functions.or(fTrue)());
  assertCallOrderAndReset(['fTrue']);

  assert(goog.functions.or(fTrue, gFalse)());
  assertCallOrderAndReset(['fTrue']);

  assert(goog.functions.or(fTrue, gFalse, hTrue)());
  assertCallOrderAndReset(['fTrue']);

  assert(goog.functions.or(goog.functions.identity)(true));
  assertFalse(goog.functions.or(goog.functions.identity)(false));
}

function testNot() {
  assertTrue(goog.functions.not(gFalse)());
  assertCallOrderAndReset(['gFalse']);

  assertTrue(goog.functions.not(goog.functions.identity)(false));
  assertFalse(goog.functions.not(goog.functions.identity)(true));

  var f = function(a, b) {
    assertEquals(1, a);
    assertEquals(2, b);
    return false;
  };

  assertTrue(goog.functions.not(f)(1, 2));
}

function testCreate(expectedArray) {
  var tempConstructor = function(a, b) {
    this.foo = a;
    this.bar = b;
  };

  var factory = goog.partial(goog.functions.create, tempConstructor, 'baz');
  var instance = factory('qux');

  assert(instance instanceof tempConstructor);
  assertEquals(instance.foo, 'baz');
  assertEquals(instance.bar, 'qux');
}

function testWithReturnValue() {
  var obj = {};
  var f = function(a, b) {
    assertEquals(obj, this);
    assertEquals(1, a);
    assertEquals(2, b);
  };
  assertTrue(goog.functions.withReturnValue(f, true).call(obj, 1, 2));
  assertFalse(goog.functions.withReturnValue(f, false).call(obj, 1, 2));
}

function testEqualTo() {
  assertTrue(goog.functions.equalTo(42)(42));
  assertFalse(goog.functions.equalTo(42)(13));
  assertFalse(goog.functions.equalTo(42)('a string'));

  assertFalse(goog.functions.equalTo(42)('42'));
  assertTrue(goog.functions.equalTo(42, true)('42'));

  assertTrue(goog.functions.equalTo(0)(0));
  assertFalse(goog.functions.equalTo(0)(''));
  assertFalse(goog.functions.equalTo(0)(1));

  assertTrue(goog.functions.equalTo(0, true)(0));
  assertTrue(goog.functions.equalTo(0, true)(''));
  assertFalse(goog.functions.equalTo(0, true)(1));
}

function makeCallOrderLogger(name, returnValue) {
  return function() {
    callOrder.push(name);
    return returnValue;
  };
}

function assertCallOrderAndReset(expectedArray) {
  assertArrayEquals(expectedArray, callOrder);
  callOrder = [];
}

function testCacheReturnValue() {
  var returnFive = function() { return 5; };

  var recordedReturnFive = goog.testing.recordFunction(returnFive);
  var cachedRecordedReturnFive =
      goog.functions.cacheReturnValue(recordedReturnFive);

  assertEquals(0, recordedReturnFive.getCallCount());
  assertEquals(5, cachedRecordedReturnFive());
  assertEquals(1, recordedReturnFive.getCallCount());
  assertEquals(5, cachedRecordedReturnFive());
  assertEquals(1, recordedReturnFive.getCallCount());
}


function testCacheReturnValueFlagEnabled() {
  var count = 0;
  var returnIncrementingInteger = function() {
    count++;
    return count;
  };

  var recordedFunction = goog.testing.recordFunction(returnIncrementingInteger);
  var cachedRecordedFunction =
      goog.functions.cacheReturnValue(recordedFunction);

  assertEquals(0, recordedFunction.getCallCount());
  assertEquals(1, cachedRecordedFunction());
  assertEquals(1, recordedFunction.getCallCount());
  assertEquals(1, cachedRecordedFunction());
  assertEquals(1, recordedFunction.getCallCount());
  assertEquals(1, cachedRecordedFunction());
}


function testCacheReturnValueFlagDisabled() {
  stubs.set(goog.functions, 'CACHE_RETURN_VALUE', false);

  var count = 0;
  var returnIncrementingInteger = function() {
    count++;
    return count;
  };

  var recordedFunction = goog.testing.recordFunction(returnIncrementingInteger);
  var cachedRecordedFunction =
      goog.functions.cacheReturnValue(recordedFunction);

  assertEquals(0, recordedFunction.getCallCount());
  assertEquals(1, cachedRecordedFunction());
  assertEquals(1, recordedFunction.getCallCount());
  assertEquals(2, cachedRecordedFunction());
  assertEquals(2, recordedFunction.getCallCount());
  assertEquals(3, cachedRecordedFunction());
}


function testOnce() {
  var recordedFunction = goog.testing.recordFunction();
  var f = goog.functions.once(recordedFunction);

  assertEquals(0, recordedFunction.getCallCount());
  f();
  assertEquals(1, recordedFunction.getCallCount());
  f();
  assertEquals(1, recordedFunction.getCallCount());
}


function testDebounce() {
  // Encoded sequences of commands to perform mapped to expected # of calls.
  //   f: fire
  //   w: wait (for the timer to elapse)
  assertAsyncDecoratorCommandSequenceCalls(goog.functions.debounce, {
    'f': 0,
    'ff': 0,
    'fff': 0,
    'fw': 1,
    'ffw': 1,
    'fffw': 1,
    'fwffwf': 2,
    'ffwwwffwwfwf': 3
  });
}


function testDebounceScopeBinding() {
  var interval = 500;
  var mockClock = new goog.testing.MockClock(true);

  var x = {'y': 0};
  goog.functions.debounce(function() { ++this['y']; }, interval, x)();
  assertEquals(0, x['y']);

  mockClock.tick(interval);
  assertEquals(1, x['y']);

  mockClock.uninstall();
}


function testDebounceArgumentBinding() {
  var interval = 500;
  var mockClock = new goog.testing.MockClock(true);

  var calls = 0;
  var debouncedFn = goog.functions.debounce(function(a, b, c) {
    ++calls;
    assertEquals(3, a);
    assertEquals('string', b);
    assertEquals(false, c);
  }, interval);

  debouncedFn(3, 'string', false);
  mockClock.tick(interval);
  assertEquals(1, calls);

  // goog.functions.debounce should always pass the last arguments passed to the
  // decorator into the decorated function, even if called multiple times.
  debouncedFn();
  mockClock.tick(interval / 2);
  debouncedFn(8, null, true);
  debouncedFn(3, 'string', false);
  mockClock.tick(interval);
  assertEquals(2, calls);

  mockClock.uninstall();
}


function testDebounceArgumentAndScopeBinding() {
  var interval = 500;
  var mockClock = new goog.testing.MockClock(true);

  var x = {'calls': 0};
  var debouncedFn = goog.functions.debounce(function(a, b, c) {
    ++this['calls'];
    assertEquals(3, a);
    assertEquals('string', b);
    assertEquals(false, c);
  }, interval, x);

  debouncedFn(3, 'string', false);
  mockClock.tick(interval);
  assertEquals(1, x['calls']);

  // goog.functions.debounce should always pass the last arguments passed to the
  // decorator into the decorated function, even if called multiple times.
  debouncedFn();
  mockClock.tick(interval / 2);
  debouncedFn(8, null, true);
  debouncedFn(3, 'string', false);
  mockClock.tick(interval);
  assertEquals(2, x['calls']);

  mockClock.uninstall();
}


function testThrottle() {
  // Encoded sequences of commands to perform mapped to expected # of calls.
  //   f: fire
  //   w: wait (for the timer to elapse)
  assertAsyncDecoratorCommandSequenceCalls(goog.functions.throttle, {
    'f': 1,
    'ff': 1,
    'fff': 1,
    'fw': 1,
    'ffw': 2,
    'fwf': 2,
    'fffw': 2,
    'fwfff': 2,
    'fwfffw': 3,
    'fwffwf': 3,
    'ffwf': 2,
    'ffwff': 2,
    'ffwfw': 3,
    'ffwffwf': 3,
    'ffwffwff': 3,
    'ffwffwffw': 4,
    'ffwwwffwwfw': 5,
    'ffwwwffwwfwf': 6
  });
}


function testThrottleScopeBinding() {
  var interval = 500;
  var mockClock = new goog.testing.MockClock(true);

  var x = {'y': 0};
  goog.functions.throttle(function() { ++this['y']; }, interval, x)();
  assertEquals(1, x['y']);

  mockClock.uninstall();
}


function testThrottleArgumentBinding() {
  var interval = 500;
  var mockClock = new goog.testing.MockClock(true);

  var calls = 0;
  var throttledFn = goog.functions.throttle(function(a, b, c) {
    ++calls;
    assertEquals(3, a);
    assertEquals('string', b);
    assertEquals(false, c);
  }, interval);

  throttledFn(3, 'string', false);
  assertEquals(1, calls);

  // goog.functions.throttle should always pass the last arguments passed to the
  // decorator into the decorated function, even if called multiple times.
  throttledFn();
  mockClock.tick(interval / 2);
  throttledFn(8, null, true);
  throttledFn(3, 'string', false);
  mockClock.tick(interval);
  assertEquals(2, calls);

  mockClock.uninstall();
}


function testThrottleArgumentAndScopeBinding() {
  var interval = 500;
  var mockClock = new goog.testing.MockClock(true);

  var x = {'calls': 0};
  var throttledFn = goog.functions.throttle(function(a, b, c) {
    ++this['calls'];
    assertEquals(3, a);
    assertEquals('string', b);
    assertEquals(false, c);
  }, interval, x);

  throttledFn(3, 'string', false);
  assertEquals(1, x['calls']);

  // goog.functions.throttle should always pass the last arguments passed to the
  // decorator into the decorated function, even if called multiple times.
  throttledFn();
  mockClock.tick(interval / 2);
  throttledFn(8, null, true);
  throttledFn(3, 'string', false);
  mockClock.tick(interval);
  assertEquals(2, x['calls']);

  mockClock.uninstall();
}


function testRateLimit() {
  // Encoded sequences of commands to perform mapped to expected # of calls.
  //   f: fire
  //   w: wait (for the timer to elapse)
  assertAsyncDecoratorCommandSequenceCalls(goog.functions.rateLimit, {
    'f': 1,
    'ff': 1,
    'fff': 1,
    'fw': 1,
    'ffw': 1,
    'fwf': 2,
    'fffw': 1,
    'fwfff': 2,
    'fwfffw': 2,
    'fwffwf': 3,
    'ffwf': 2,
    'ffwff': 2,
    'ffwfw': 2,
    'ffwffwf': 3,
    'ffwffwff': 3,
    'ffwffwffw': 3,
    'ffwwwffwwfw': 3,
    'ffwwwffwwfwf': 4
  });
}


function testRateLimitScopeBinding() {
  var interval = 500;
  var mockClock = new goog.testing.MockClock(true);

  var x = {'y': 0};
  goog.functions.rateLimit(function() {
    ++this['y'];
  }, interval, x)();
  assertEquals(1, x['y']);

  mockClock.uninstall();
}


function testRateLimitArgumentBinding() {
  var interval = 500;
  var mockClock = new goog.testing.MockClock(true);

  var calls = 0;
  var rateLimitedFn = goog.functions.rateLimit(function(a, b, c) {
    ++calls;
    assertEquals(3, a);
    assertEquals('string', b);
    assertEquals(false, c);
  }, interval);

  rateLimitedFn(3, 'string', false);
  assertEquals(1, calls);

  // goog.functions.rateLimit should always pass the first arguments passed to
  // the
  // decorator into the decorated function, even if called multiple times.
  rateLimitedFn();
  mockClock.tick(interval / 2);
  rateLimitedFn(8, null, true);
  mockClock.tick(interval);
  rateLimitedFn(3, 'string', false);
  assertEquals(2, calls);

  mockClock.uninstall();
}


function testRateLimitArgumentAndScopeBinding() {
  var interval = 500;
  var mockClock = new goog.testing.MockClock(true);

  var x = {'calls': 0};
  var rateLimitedFn = goog.functions.rateLimit(function(a, b, c) {
    ++this['calls'];
    assertEquals(3, a);
    assertEquals('string', b);
    assertEquals(false, c);
  }, interval, x);

  rateLimitedFn(3, 'string', false);
  assertEquals(1, x['calls']);

  // goog.functions.rateLimit should always pass the last arguments passed to
  // the
  // decorator into the decorated function, even if called multiple times.
  rateLimitedFn();
  mockClock.tick(interval / 2);
  rateLimitedFn(8, null, true);
  mockClock.tick(interval);
  rateLimitedFn(3, 'string', false);
  assertEquals(2, x['calls']);

  mockClock.uninstall();
}


/**
 * Wraps a {@code goog.testing.recordFunction} with the specified decorator and
 * executes a list of command sequences, asserting that in each case the
 * decorated function is called the expected number of times.
 * @param {function():*} decorator The async decorator to test.
 * @param {!Object.<string, number>} expectedCommandSequenceCalls An object
 *     mapping string command sequences (where 'f' is 'fire' and 'w' is 'wait')
 *     to the number times we expect a decorated function to be called during
 *     the execution of those commands.
 */
function assertAsyncDecoratorCommandSequenceCalls(
    decorator, expectedCommandSequenceCalls) {
  var interval = 500;

  var mockClock = new goog.testing.MockClock(true);
  for (var commandSequence in expectedCommandSequenceCalls) {
    var recordedFunction = goog.testing.recordFunction();
    var f = decorator(recordedFunction, interval);

    for (var i = 0; i < commandSequence.length; ++i) {
      switch (commandSequence[i]) {
        case 'f':
          f();
          break;
        case 'w':
          mockClock.tick(interval);
          break;
      }
    }

    var expectedCalls = expectedCommandSequenceCalls[commandSequence];
    assertEquals(
        'Expected ' + expectedCalls + ' calls for command sequence "' +
            commandSequence + '" (' +
            goog.array
                .map(
                    commandSequence,
                    function(command) {
                      switch (command) {
                        case 'f':
                          return 'fire';
                        case 'w':
                          return 'wait';
                      }
                    })
                .join(' -> ') +
            ')',
        expectedCalls, recordedFunction.getCallCount());
  }
  mockClock.uninstall();
}
