require 'luacov'
local _ = require 'moses'

context('Object functions specs', function()

  context('keys', function()
  
    test('collects a given object attributes',function()
      assert_true(_.isEqual(_.keys({1,2,3}),{1,2,3}))
      assert_true(_.isEqual(_.keys({4,5,6}),{1,2,3}))
      assert_true(_.same(_.keys({x = 1, y = 2, 3}),{'x','y',1}))
    end)
    
  end)
  
  context('values', function()
  
    test('collects an given object values',function()
      assert_true(_.isEqual(_.values({1,2,3}),{1,2,3}))
      assert_true(_.isEqual(_.values({4,5,6}),{4,5,6}))
      assert_true(_.same(_.values({x = 1, y = 2, 3}),{1,2,3}))
    end)
   
  end)  
  
  context('toBoolean', function()
  
    test('converts a value to a boolean',function()
      assert_true(type(_.toBoolean(true)) == 'boolean')
      assert_true(type(_.toBoolean(1)) == 'boolean')
      assert_true(type(_.toBoolean(false)) == 'boolean')
      assert_true(type(_.toBoolean(nil)) == 'boolean')
      assert_true(type(_.toBoolean({})) == 'boolean')
      assert_true(type(_.toBoolean(1/0)) == 'boolean')
      
      assert_true(_.toBoolean(true))
      assert_true(_.toBoolean(1))
      assert_false(_.toBoolean(false))
      assert_false(_.toBoolean(nil))
      assert_true(_.toBoolean({}))
      assert_true(_.toBoolean(1/0))      
    end)
  
  end)  
  
  context('extend', function()
  
    test('extends a destination objects with key-values a source object',function()
      assert_true(_.isEqual(_.extend({},{a = 'b'}),{a = 'b'}))
    end)
    
    test('source properties overrides destination properties',function()
      assert_true(_.isEqual(_.extend({a = 'a'},{a = 'b'}),{a = 'b'}))
    end)   

    test('leaves source object untouched',function()
      local source = {i = 'i'}
      assert_true(_.isEqual(_.extend({a = 'a'},source),{a = 'a',i = 'i'}))
      assert_true(_.isEqual(source,{i = 'i'}))
    end)

    test('can extend destination from multiple sources',function()
      local sourceA = {a = 'a'}; local sourceBC = {b = 'b', c = 'c'} 
      assert_true(_.isEqual(_.extend({},sourceA, sourceBC),{a = 'a', b = 'b', c = 'c'}))
    end) 

    test('extending from multiple source, latter properties overrides',function()
      local sourceA = {a = 'a'}; local sourceBC = {b = 'b', a = 'c'} 
      assert_true(_.isEqual(_.extend({},sourceA, sourceBC),{a = 'c', b = 'b'}))
    end)     
    
    test('will not copy nil values',function()
      local sourceA = {a = nil}; local sourceBC = {b = 'b', c = nil} 
      assert_true(_.isEqual(_.extend({},sourceA, sourceBC),{b = 'b'}))
    end)    
  end)
  
  context('functions', function()

    test('collects function names within an object',function()
      local x = {}
      function x.a() return end; function x.b() return end    
      assert_true(_.isEqual(_.functions(x),{'a','b'}))
    end)
    
    test('collects metatable functions if "recurseMt" arg is supplied',function()
      local x = {} ; x.__index = x
      function x.a() return end; function x.b() return end
      local xx = setmetatable({},x)
      function xx.c() return end
      assert_true(_.same(_.functions(xx),{'c'}))
      assert_true(_.same(_.functions(xx,true),{'a','b','c'}))
    end)

    test('when given no obj as argument, returns all library functions',function()
      local functions = _.functions()
      _.each(functions, function(k,v)
        assert_true(_.isFunction(_[v]))
      end)
    end)
    
  end)  
  
  context('clone', function()
  
    test('clones the attributes of an object',function()
      local vector = {x = 0, y = 0}
      assert_true(_.isEqual(_.clone(vector),vector))
    end)
    
    test('By default, cloning is deep (clones nested tables)',function()
      local particle = {position = {x = 0,y=0},mass = 1}
      local particle_clone = _.clone (particle)
      assert_true(_.isEqual(particle_clone,particle))
      particle_clone.position.x = 3
      assert_false(_.isEqual(particle_clone,particle))
    end)
 
    test('Unless "shallow" arg is provided',function()
      local particle = {position = {x = 0,y=0},mass = 1}
      local particle_clone = _.clone (particle,true)
      assert_true(_.isEqual(particle_clone,particle))
      particle_clone.position.x = 3
      assert_true(_.isEqual(particle_clone,particle))
    end) 
    
    test('Non objects are simply returned',function()
      assert_equal(_.clone(1),1)
      assert_equal(_.clone(false),false)
      assert_equal(_.clone(true),true)
      assert_equal(_.clone(nil),nil)
      assert_equal(_.clone('hello'),'hello')
      assert_equal(_.clone(print),print)
    end)     
    
  end)
  
  context('tap', function()
    
    test('tap-into a method chain', function()
      local t = {}
      local catchMax = function(k) t[#t+1] = _.max(k) end
      local catchMin = function(k) t[#t+1] = _.min(k) end
      
      _.chain({1,2,3})
        :map(function(i,j) return j*2 end)
        :tap(catchMax)
        :map(function(i,k) return k^2 end)
        :tap(catchMin)
        :value()
        
      assert_equal(t[1],6)
      assert_equal(t[2],4)
    end)
  
  end)

  context('has', function()
  
    test('checks if an object has an attribute',function()
      assert_true(_.has(_,'has'))
      assert_true(_.has(table,'concat'))
      assert_true(_.has(string,'format'))
      assert_true(_.has(os,'time'))
      assert_true(_.has(math,'random'))
    end)
    
  end) 
  
  context('pick', function()
  
    test('collect specified properties',function()
      local object = {a = 1, b = 2, c = 3}
      assert_true(_.isEqual(_.pick(object,'a','c'),{a = 1, c = 3}))
    end)
    
    test('given args can be nested as well',function()
      local object = {a = 1, b = 2, c = 3}
      assert_true(_.isEqual(_.pick(object,{{'b','a'}},'c'),{a = 1,b = 2, c = 3}))
    end)

    test('will ignore properties the object do not have',function()
      local object = {a = 1, b = 2, c = 3}
      assert_true(_.isEqual(_.pick(object,{{'k'}},'c'),{c = 3}))
    end) 

    test('returns an empty table when given no properties to pick',function()
      local object = {a = 1, b = 2, c = 3}
      assert_true(_.isEqual(_.pick(object),{}))
    end)  

    test('should also pick attributes having falsy values',function()
      local object = {a = false, b = false, c = true}
      assert_true(_.isEqual(_.pick(object,'a','b'),{a = false,b = false}))
    end)  
    
  end)  
  
  context('omit', function()
  
    test('collect all properties leaving those given as args',function()
      local object = {a = 1, b = 2, c = 3}
      assert_true(_.isEqual(_.omit(object,'a','c'),{b=2}))
    end)
    
    test('given args can be nested as well',function()
      local object = {a = 1, b = 2, c = 3}
      assert_true(_.isEqual(_.omit(object,{{'b'}},'c'),{a = 1}))
    end)

    test('will ignore properties the object do not have',function()
      local object = {a = 1, b = 2, c = 3}
      assert_true(_.isEqual(_.omit(object,{{'k'}},'c'),{a = 1, b=2}))
    end) 

    test('returns the original object clone when given no properties to omit',function()
      local object = {a = 1, b = 2, c = 3}
      assert_true(_.isEqual(_.omit(object),{a = 1, b = 2, c = 3}))
    end)
    
  end)
  
  context('template', function()
  
    test('applies a template on an object',function()
      assert_true(_.isEqual(_.template({},{a = 1, b = 2, c = 3}),{a = 1, b = 2, c = 3}))
    end)
    
    test('does not override existing properies',function()
      assert_true(_.isEqual(_.template({a = 10, b = 10},{a = 1, b = 2, c = 3}),{a = 10, b = 10, c = 3}))
    end)

    test('returns the object when given no template arg',function()
      assert_true(_.isEqual(_.template({a = 10, b = 10}),{a = 10, b = 10}))
    end)
    
  end) 

  context('isEqual', function()
  
    test('compares values',function()
      assert_true(_.isEqual(1,1))
      assert_true(_.isEqual(1.0,1))
      assert_false(_.isEqual(1,2))
      assert_false(_.isEqual(2,2.0001))
    end)
    
    test('compares objects by reference and components',function()
      local oldprint = print
      assert_true(_.isEqual(print,oldprint))
      
      local t = {}
      local v = t
      assert_true(_.isEqual(t,v))
      assert_true(_.isEqual({},{}))
      
      assert_false(_.isEqual('a','b'))
      
      assert_false(_.isEqual(true, false))
      assert_false(_.isEqual(nil, false))
      assert_false(_.isEqual(true, nil))       
    
    end)

    test('compares nested properties',function()
      assert_true(_.isEqual({x = 0,{x1 = 0,{x2 =0}}}, {x = 0,{x1 = 0,{x2 =0}}}))
      assert_false(_.isEqual({x = 0,{x1 = 0,{x2 =0}}}, {x = 0,{x1 = 0,{x2 =1}}}))
    end)

    test('can compare tables on the basis of their metatable',function()
      local a, b = {x = 1, y = 2}, {x = 2, y = 1}
      setmetatable(a,{__eq = function(a,b) return (a.x and b.x and a.y and b.y)~=nil end})
      assert_false(_.isEqual(a, b))
      assert_true(_.isEqual(a, b, true))
    end)
  
    
  end)

  context('result', function()
  
    test('calls an object method, passing it as a first arg the object itself',function()
     assert_equal(_.result('a','len'),1)
     assert_equal(_.result('hello','reverse'),'olleh')
     assert_equal(_.result({'a','b','c'},table.concat),'abc')
    end)
    
    test('handles extra-args to be passed to the so-called method',function()
     assert_equal(_.result('Hello','match','%u'),'H')
     assert_equal(_.result({'a','b','c'},table.concat,' '),'a b c')
    end)    
    
    test('returns the property itself if not callable',function()
     assert_equal(_.result({size = 0},'size'),0)
    end)
     
  end)

  context('isTable', function()
  
    test('returns "true" if arg is table or array',function()
      assert_true(_.isTable({}))
      assert_true(_.isTable({1,2}))
      assert_true(_.isTable({x = 1, 2}))
      assert_true(_.isTable(string))
      assert_true(_.isTable(table))
      assert_true(_.isTable(math))
      
    end)
    
    test('returns "false" otherwise',function()
      assert_false(_.isTable(1))
      assert_false(_.isTable(''))
      assert_false(_.isTable(function() end))
      assert_false(_.isTable(print))
      assert_false(_.isTable(false))
      assert_false(_.isTable(nil))
      assert_false(_.isTable(true))      
    end)
    
  end) 

  context('isCallable', function()
  
    test('returns "true" if arg is callable',function()
      assert_true(_.isCallable(print))
      assert_true(_.isCallable(function() end))
      assert_true(_.isCallable(string.gmatch))
      assert_true(_.isCallable(setmetatable({},{__index = string}).upper))      
      assert_true(_.isCallable(setmetatable({},{__call = function() return end})))      
    end)
    
    test('returns "false" otherwise',function()
      assert_false(_.isCallable(1))
      assert_false(_.isCallable(''))
      assert_false(_.isCallable({}))
      assert_false(_.isCallable(false))
      assert_false(_.isCallable(nil))
      assert_false(_.isCallable(true))      
    end)
    
  end) 

  context('isArray', function()
  
    test('returns "true" if arg is an array',function()
      assert_true(_.isArray({}))
      assert_true(_.isArray({1,2,3}))
      assert_true(_.isArray({'a','b','c',{}}))
      assert_true(_.isArray({false,true}))
      assert_true(_.isArray({1,nil}))
    end)
   
    test('returns "false" otherwise',function()
      assert_false(_.isArray(1))
      assert_false(_.isArray(''))
      assert_false(_.isArray(false))
      assert_false(_.isArray(nil))
      assert_false(_.isArray(true))      
      assert_false(_.isArray(print))
      assert_false(_.isArray({a = 1, x = 1}))
      assert_false(_.isArray({a = 1, 1, 2,3}))
      assert_false(_.isArray({1,nil,2}))
      assert_false(_.isArray({1,nil,3,k=4}))
      assert_false(_.isArray({a=1}))
    end)   
   
    test('returns false on "sparse arrays"',function()
      assert_false(_.isArray({[1] = true, [10] = false}))
   end)   
  
  end)
  
  context('isIterable', function()
  
    test('checks if the given object is iterable with pairs',function()
      assert_true(_.isIterable({}))
      assert_false(_.isIterable(function() end))
      assert_false(_.isIterable(false))
      assert_false(_.isIterable(1))
    end)
    
  end)  
     
  context('isEmpty', function()
  
    test('returns "true" if arg is an empty array',function()
      assert_true(_.isEmpty({}))      
    end)
    
    test('returns "false" otherwise',function()
      assert_false(_.isEmpty({1,2,3}))
      assert_false(_.isEmpty({'a','b','c',{}}))
      assert_false(_.isEmpty({nil,false,true}))
    end)

    test('booleans, nil and functions are considered empty',function()
      assert_true(_.isEmpty(print))
      assert_true(_.isEmpty(nil))
      assert_true(_.isEmpty(false))
      assert_true(_.isEmpty(true))
    end)
    
    test('handles strings',function()
      assert_true(_.isEmpty(''))
      assert_false(_.isEmpty('a'))
      assert_false(_.isEmpty('bcd'))
      assert_false(_.isEmpty(' '))
    end)  
    
  end) 

  context('isString', function()
  
    test('returns "true" if arg is a string',function()
      assert_true(_.isString(''))
      assert_true(_.isString('a'))
      assert_true(_.isString(' '))
      assert_true(_.isString(type(nil)))
    end)
    
    test('returns "false" otherwise',function()
      assert_false(_.isString(false))
      assert_false(_.isString(print))
      assert_false(_.isString(nil))
      assert_false(_.isString(true))
      assert_false(_.isString({}))
    end)
    
  end)

  context('isFunction', function()
  
    test('returns "true" if arg is a function',function()
      assert_true(_.isFunction(print))
      assert_true(_.isFunction(string.match))
      assert_true(_.isFunction(function() end))
    end)

    test('returns "false" otherwise',function()
      assert_false(_.isFunction({}))
      assert_false(_.isFunction(nil))
      assert_false(_.isFunction(false))
      assert_false(_.isFunction(true))
      assert_false(_.isFunction('a'))
    end)    
    
  end)
  
  context('isNil', function()
  
    test('returns "true" if arg is nil',function()
      assert_true(_.isNil(nil))
      assert_true(_.isNil())
      assert_true(_.isNil(a))
    end)
    
    test('returns "false" otherwise',function()
      assert_false(_.isNil(false))
      assert_false(_.isNil(true))
      assert_false(_.isNil(table))
      assert_false(_.isNil(function() end))
      assert_false(_.isNil('a'))
    end)     
    
  end) 

  context('isNumber', function()
  
    test('returns "true" if arg is a number',function()
      assert_true(_.isNumber(1))
      assert_true(_.isNumber(0.5))
      assert_true(_.isNumber(math.pi))
      assert_true(_.isNumber(1/0))
      assert_true(_.isNumber(math.huge))
      assert_true(_.isNumber(0/0))
    end)
    
    test('returns "false" otherwise',function()
      assert_false(_.isNumber(print))
      assert_false(_.isNumber(nil))
      assert_false(_.isNumber(true))
      assert_false(_.isNumber(false))
      assert_false(_.isNumber({1}))
      assert_false(_.isNumber('1'))
    end)    
    
  end)

  context('isNaN', function()
  
    test('returns "true" if arg is NaN',function()
      assert_true(_.isNaN(0/0))  
    end)
    
    test('returns "false" for not NaN values',function()
      assert_false(_.isNaN(1/0))  
      assert_false(_.isNaN(math.huge))
      assert_false(_.isNaN(math.pi))
      assert_false(_.isNaN(1))
      assert_false(_.isNaN(''))
      assert_false(_.isNaN('0'))
      assert_false(_.isNaN({}))
      assert_false(_.isNaN(nil))
      assert_false(_.isNaN(false))
      assert_false(_.isNaN(true))
    end)    
    
  end) 
  
  context('isFinite', function()
  
    test('returns "true" if arg is a finite number',function()
      assert_true(_.isFinite(1))
      assert_true(_.isFinite(0))
      assert_true(_.isFinite(math.pi))
      assert_true(_.isFinite(99e99))
    end)
    
    test('returns "false" otherwise',function()
      assert_false(_.isFinite(math.huge))
      assert_false(_.isFinite(1/0))
      assert_false(_.isFinite(0/0))
    end) 

    test('returns "false" for non-numbers',function()
      assert_false(_.isFinite(''))
      assert_false(_.isFinite(function() end))
      assert_false(_.isFinite({}))
    end)    
    
  end)

  context('isBoolean', function()
 
    test('returns "true" if arg is a boolean or a thruthy statement',function()
      assert_true(_.isBoolean(true))
      assert_true(_.isBoolean(false))
      assert_true(_.isBoolean(1==1))
      assert_true(_.isBoolean(print==tostring))     
    end)
    
    test('returns "false" otherwise',function()
      assert_false(_.isBoolean(''))
      assert_false(_.isBoolean(nil))
      assert_false(_.isBoolean({}))
      assert_false(_.isBoolean(function() end))

      assert_false(_.isBoolean(0))
      assert_false(_.isBoolean('1'))
    end)    
    
  end)
  
  context('isInteger', function()
  
    test('returns "true" if arg is a integer, "false" otherwise',function()
      assert_true(_.isInteger(1))
      assert_true(_.isInteger(0))
      assert_false(_.isInteger(math.pi))
      assert_true(_.isInteger(1/0))
      assert_true(_.isInteger(math.huge))
      assert_false(_.isInteger(0/0))
    end)  
    
  end)
  
end)