// Copyright 2006 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.objectTest');
goog.setTestOnly('goog.objectTest');

goog.require('goog.functions');
goog.require('goog.object');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.recordFunction');

function stringifyObject(m) {
  var keys = goog.object.getKeys(m);
  var s = '';
  for (var i = 0; i < keys.length; i++) {
    s += keys[i] + goog.object.get(m, keys[i]);
  }
  return s;
}

function getObject() {
  return {
    a: 0,
    b: 1,
    c: 2,
    d: 3
  };
}

function testKeys() {
  var m = getObject();
  assertEquals('getKeys, The keys should be a,b,c',
               'a,b,c,d',
               goog.object.getKeys(m).join(','));
}

function testValues() {
  var m = getObject();
  assertEquals('getValues, The values should be 0,1,2',
      '0,1,2,3', goog.object.getValues(m).join(','));
}

function testGetAnyKey() {
  var m = getObject();
  assertTrue('getAnyKey, The key should be a,b,c or d',
             goog.object.getAnyKey(m) in m);
  assertUndefined('getAnyKey, The key should be undefined',
                  goog.object.getAnyKey({}));
}

function testGetAnyValue() {
  var m = getObject();
  assertTrue('getAnyValue, The value should be 0,1,2 or 3',
             goog.object.containsValue(m, goog.object.getAnyValue(m)));
  assertUndefined('getAnyValue, The value should be undefined',
                  goog.object.getAnyValue({}));
}

function testContainsKey() {
  var m = getObject();
  assertTrue("containsKey, Should contain the 'a' key",
             goog.object.containsKey(m, 'a'));
  assertFalse("containsKey, Should not contain the 'e' key",
              goog.object.containsKey(m, 'e'));
}

function testContainsValue() {
  var m = getObject();
  assertTrue('containsValue, Should contain the value 0',
             goog.object.containsValue(m, 0));
  assertFalse('containsValue, Should not contain the value 4',
              goog.object.containsValue(m, 4));
  assertTrue('isEmpty, The map should not be empty', !goog.object.isEmpty(m));
}

function testFindKey() {
  var dict = {'a': 1, 'b': 2, 'c': 3, 'd': 4};
  var key = goog.object.findKey(dict, function(v, k, d) {
    assertEquals('valid 3rd argument', dict, d);
    assertTrue('valid 1st argument', goog.object.containsValue(d, v));
    assertTrue('valid 2nd argument', k in d);
    return v % 3 == 0;
  });
  assertEquals('key "c" found', 'c', key);

  var pred = function(value) {
    return value > 5;
  };
  assertUndefined('no match', goog.object.findKey(dict, pred));
}

function testFindValue() {
  var dict = {'a': 1, 'b': 2, 'c': 3, 'd': 4};
  var value = goog.object.findValue(dict, function(v, k, d) {
    assertEquals('valid 3rd argument', dict, d);
    assertTrue('valid 1st argument', goog.object.containsValue(d, v));
    assertTrue('valid 2nd argument', k in d);
    return k.toUpperCase() == 'C';
  });
  assertEquals('value 3 found', 3, value);

  var pred = function(value, key) {
    return key > 'd';
  };
  assertUndefined('no match', goog.object.findValue(dict, pred));
}

function testClear() {
  var m = getObject();
  goog.object.clear(m);
  assertTrue('cleared so it should be empty', goog.object.isEmpty(m));
  assertFalse("cleared so it should not contain 'a' key",
              goog.object.containsKey(m, 'a'));
}

function testClone() {
  var m = getObject();
  var m2 = goog.object.clone(m);
  assertFalse('clone so it should not be empty', goog.object.isEmpty(m2));
  assertTrue("clone so it should contain 'c' key",
             goog.object.containsKey(m2, 'c'));
}

function testUnsafeClonePrimitive() {
  assertEquals('cloning a primitive should return an equal primitive',
      5, goog.object.unsafeClone(5));
}

function testUnsafeCloneObjectThatHasACloneMethod() {
  var original = {
    name: 'original',
    clone: goog.functions.constant({name: 'clone'})
  };

  var clone = goog.object.unsafeClone(original);
  assertEquals('original', original.name);
  assertEquals('clone', clone.name);
}

function testUnsafeCloneFlatObject() {
  var original = {a: 1, b: 2, c: 3};
  var clone = goog.object.unsafeClone(original);
  assertNotEquals(original, clone);
  assertObjectEquals(original, clone);
}

function testUnsafeCloneDeepObject() {
  var original = {
    a: 1,
    b: {c: 2, d: 3},
    e: {f: {g: 4, h: 5}}
  };
  var clone = goog.object.unsafeClone(original);

  assertNotEquals(original, clone);
  assertNotEquals(original.b, clone.b);
  assertNotEquals(original.e, clone.e);

  assertEquals(1, clone.a);
  assertEquals(2, clone.b.c);
  assertEquals(3, clone.b.d);
  assertEquals(4, clone.e.f.g);
  assertEquals(5, clone.e.f.h);
}

function testUnsafeCloneFunctions() {
  var original = {
    f: goog.functions.constant('hi')
  };
  var clone = goog.object.unsafeClone(original);

  assertNotEquals(original, clone);
  assertEquals('hi', clone.f());
  assertEquals(original.f, clone.f);
}

function testForEach() {
  var m = getObject();
  var s = '';
  goog.object.forEach(m, function(val, key, m2) {
    assertNotUndefined(key);
    assertEquals(m, m2);
    s += key + val;
  });
  assertEquals(s, 'a0b1c2d3');
}

function testFilter() {
  var m = getObject();

  var m2 = goog.object.filter(m, function(val, key, m3) {
    assertNotUndefined(key);
    assertEquals(m, m3);
    return val > 1;
  });
  assertEquals(stringifyObject(m2), 'c2d3');
}


function testMap() {
  var m = getObject();
  var m2 = goog.object.map(m, function(val, key, m3) {
    assertNotUndefined(key);
    assertEquals(m, m3);
    return val * val;
  });
  assertEquals(stringifyObject(m2), 'a0b1c4d9');
}

function testSome() {
  var m = getObject();
  var b = goog.object.some(m, function(val, key, m2) {
    assertNotUndefined(key);
    assertEquals(m, m2);
    return val > 1;
  });
  assertTrue(b);
  var b = goog.object.some(m, function(val, key, m2) {
    assertNotUndefined(key);
    assertEquals(m, m2);
    return val > 100;
  });
  assertFalse(b);
}

function testEvery() {
  var m = getObject();
  var b = goog.object.every(m, function(val, key, m2) {
    assertNotUndefined(key);
    assertEquals(m, m2);
    return val >= 0;
  });
  assertTrue(b);
  b = goog.object.every(m, function(val, key, m2) {
    assertNotUndefined(key);
    assertEquals(m, m2);
    return val > 1;
  });
  assertFalse(b);
}

function testContains() {
  var m = getObject();
  assertTrue(goog.object.contains(m, 3));
  assertFalse(goog.object.contains(m, 4));
}

function testObjectProperties() {
  var m = {};

  goog.object.set(m, 'toString', 'once');
  goog.object.set(m, 'valueOf', 'upon');
  goog.object.set(m, 'eval', 'a');
  goog.object.set(m, 'toSource', 'midnight');
  goog.object.set(m, 'prototype', 'dreary');
  goog.object.set(m, 'hasOwnProperty', 'dark');

  assertEquals(goog.object.get(m, 'toString'), 'once');
  assertEquals(goog.object.get(m, 'valueOf'), 'upon');
  assertEquals(goog.object.get(m, 'eval'), 'a');
  assertEquals(goog.object.get(m, 'toSource'), 'midnight');
  assertEquals(goog.object.get(m, 'prototype'), 'dreary');
  assertEquals(goog.object.get(m, 'hasOwnProperty'), 'dark');
}

function testSetDefault() {
  var dict = {};
  assertEquals(1, goog.object.setIfUndefined(dict, 'a', 1));
  assertEquals(1, dict['a']);
  assertEquals(1, goog.object.setIfUndefined(dict, 'a', 2));
  assertEquals(1, dict['a']);
}

function createRecordedGetFoo() {
  return goog.testing.recordFunction(goog.functions.constant('foo'));
}

function testSetWithReturnValueNotSet_KeyIsSet() {
  var f = createRecordedGetFoo();
  var obj = {};
  obj['key'] = 'bar';
  assertEquals(
      'bar',
      goog.object.setWithReturnValueIfNotSet(obj, 'key', f));
  f.assertCallCount(0);
}

function testSetWithReturnValueNotSet_KeyIsNotSet() {
  var f = createRecordedGetFoo();
  var obj = {};
  assertEquals(
      'foo',
      goog.object.setWithReturnValueIfNotSet(obj, 'key', f));
  f.assertCallCount(1);
}

function testSetWithReturnValueNotSet_KeySetValueIsUndefined() {
  var f = createRecordedGetFoo();
  var obj = {};
  obj['key'] = undefined;
  assertEquals(
      undefined,
      goog.object.setWithReturnValueIfNotSet(obj, 'key', f));
  f.assertCallCount(0);
}

function testTranspose() {
  var m = getObject();
  var b = goog.object.transpose(m);
  assertEquals('a', b[0]);
  assertEquals('b', b[1]);
  assertEquals('c', b[2]);
  assertEquals('d', b[3]);
}

function testExtend() {
  var o = {};
  var o2 = {a: 0, b: 1};
  goog.object.extend(o, o2);
  assertEquals(0, o.a);
  assertEquals(1, o.b);
  assertTrue('a' in o);
  assertTrue('b' in o);

  o2 = {c: 2};
  goog.object.extend(o, o2);
  assertEquals(2, o.c);
  assertTrue('c' in o);

  o2 = {c: 3};
  goog.object.extend(o, o2);
  assertEquals(3, o.c);
  assertTrue('c' in o);

  o = {};
  o2 = {c: 2};
  var o3 = {c: 3};
  goog.object.extend(o, o2, o3);
  assertEquals(3, o.c);
  assertTrue('c' in o);

  o = {};
  o2 = {a: 0, b: 1};
  o3 = {c: 2, d: 3};
  goog.object.extend(o, o2, o3);
  assertEquals(0, o.a);
  assertEquals(1, o.b);
  assertEquals(2, o.c);
  assertEquals(3, o.d);
  assertTrue('a' in o);
  assertTrue('b' in o);
  assertTrue('c' in o);
  assertTrue('d' in o);

  o = {};
  o2 = {
    'constructor': 0,
    'hasOwnProperty': 1,
    'isPrototypeOf': 2,
    'propertyIsEnumerable': 3,
    'toLocaleString': 4,
    'toString': 5,
    'valueOf': 6
  };
  goog.object.extend(o, o2);
  assertEquals(0, o['constructor']);
  assertEquals(1, o['hasOwnProperty']);
  assertEquals(2, o['isPrototypeOf']);
  assertEquals(3, o['propertyIsEnumerable']);
  assertEquals(4, o['toLocaleString']);
  assertEquals(5, o['toString']);
  assertEquals(6, o['valueOf']);
  assertTrue('constructor' in o);
  assertTrue('hasOwnProperty' in o);
  assertTrue('isPrototypeOf' in o);
  assertTrue('propertyIsEnumerable' in o);
  assertTrue('toLocaleString' in o);
  assertTrue('toString' in o);
  assertTrue('valueOf' in o);
}

function testCreate() {
  assertObjectEquals('With multiple arguments',
                     {a: 0, b: 1}, goog.object.create('a', 0, 'b', 1));
  assertObjectEquals('With an array argument',
                     {a: 0, b: 1}, goog.object.create(['a', 0, 'b', 1]));

  assertObjectEquals('With no arguments',
                     {}, goog.object.create());
  assertObjectEquals('With an ampty array argument',
                     {}, goog.object.create([]));

  assertThrows('Should throw due to uneven arguments', function() {
    goog.object.create('a');
  });
  assertThrows('Should throw due to uneven arguments', function() {
    goog.object.create('a', 0, 'b');
  });
  assertThrows('Should throw due to uneven length array', function() {
    goog.object.create(['a']);
  });
  assertThrows('Should throw due to uneven length array', function() {
    goog.object.create(['a', 0, 'b']);
  });
}

function testCreateSet() {
  assertObjectEquals('With multiple arguments',
                     {a: true, b: true}, goog.object.createSet('a', 'b'));
  assertObjectEquals('With an array argument',
                     {a: true, b: true}, goog.object.createSet(['a', 'b']));

  assertObjectEquals('With no arguments',
                     {}, goog.object.createSet());
  assertObjectEquals('With an ampty array argument',
                     {}, goog.object.createSet([]));
}

function createTestDeepObject() {
  var obj = {};
  obj.a = {};
  obj.a.b = {};
  obj.a.b.c = {};
  obj.a.b.c.fooArr = [5, 6, 7, 8];
  obj.a.b.c.knownNull = null;
  return obj;
}

function testGetValueByKeys() {
  var obj = createTestDeepObject();
  assertEquals(obj, goog.object.getValueByKeys(obj));
  assertEquals(obj.a, goog.object.getValueByKeys(obj, 'a'));
  assertEquals(obj.a.b, goog.object.getValueByKeys(obj, 'a', 'b'));
  assertEquals(obj.a.b.c, goog.object.getValueByKeys(obj, 'a', 'b', 'c'));
  assertEquals(obj.a.b.c.d,
               goog.object.getValueByKeys(obj, 'a', 'b', 'c', 'd'));
  assertEquals(8, goog.object.getValueByKeys(obj, 'a', 'b', 'c', 'fooArr', 3));
  assertNull(goog.object.getValueByKeys(obj, 'a', 'b', 'c', 'knownNull'));
  assertUndefined(goog.object.getValueByKeys(obj, 'e', 'f', 'g'));
}

function testGetValueByKeysArraySyntax() {
  var obj = createTestDeepObject();
  assertEquals(obj, goog.object.getValueByKeys(obj, []));
  assertEquals(obj.a, goog.object.getValueByKeys(obj, ['a']));

  assertEquals(obj.a.b, goog.object.getValueByKeys(obj, ['a', 'b']));
  assertEquals(obj.a.b.c, goog.object.getValueByKeys(obj, ['a', 'b', 'c']));
  assertEquals(obj.a.b.c.d,
      goog.object.getValueByKeys(obj, ['a', 'b', 'c', 'd']));
  assertEquals(8,
      goog.object.getValueByKeys(obj, ['a', 'b', 'c', 'fooArr', 3]));
  assertNull(goog.object.getValueByKeys(obj, ['a', 'b', 'c', 'knownNull']));
  assertUndefined(goog.object.getValueByKeys(obj, 'e', 'f', 'g'));
}

function testImmutableView() {
  if (!Object.isFrozen) {
    return;
  }
  var x = {propA: 3};
  var y = goog.object.createImmutableView(x);
  x.propA = 4;
  x.propB = 6;
  y.propA = 5;
  y.propB = 7;
  assertEquals(4, x.propA);
  assertEquals(6, x.propB);
  assertFalse(goog.object.isImmutableView(x));

  assertEquals(4, y.propA);
  assertEquals(6, y.propB);
  assertTrue(goog.object.isImmutableView(y));

  assertFalse('x and y should be different references', x == y);
  assertTrue(
      'createImmutableView should not create a new view of an immutable object',
      y == goog.object.createImmutableView(y));
}

function testImmutableViewStrict() {
  'use strict';

  // IE9 supports isFrozen, but does not support strict mode. Exit early if we
  // are not actually running in strict mode.
  var isStrict = (function() { return !this; })();

  if (!Object.isFrozen || !isStrict) {
    return;
  }
  var x = {propA: 3};
  var y = goog.object.createImmutableView(x);
  assertThrows(function() {
    y.propA = 4;
  });
  assertThrows(function() {
    y.propB = 4;
  });
}

function testEmptyObjectsAreEqual() {
  assertTrue(goog.object.equals({}, {}));
}

function testObjectsWithDifferentKeysAreUnequal() {
  assertFalse(goog.object.equals({'a': 1}, {'b': 1}));
}

function testObjectsWithDifferentValuesAreUnequal() {
  assertFalse(goog.object.equals({'a': 1}, {'a': 2}));
}

function testObjectsWithSameKeysAndValuesAreEqual() {
  assertTrue(goog.object.equals({'a': 1}, {'a': 1}));
}

function testObjectsWithSameKeysInDifferentOrderAreEqual() {
  assertTrue(goog.object.equals({'a': 1, 'b': 2}, {'b': 2, 'a': 1}));
}
