chai      = require('chai')
expect    = chai.expect
should    = chai.should()
sinon     = require('sinon')
sinonChai = require('sinon-chai')
_         = require("underscore")

chai.use(sinonChai)

Y = require "../lib/y.coffee"
Y.Test = require "../../y-test/lib/y-test.coffee"
Y.Text = require "../../y-text/lib/y-text"
Y.List = require "../../y-list/lib/y-list"

TestSuite = require "./TestSuite"
class ObjectTest extends TestSuite

  constructor: (suffix)->
    super suffix, Y

  makeNewUser: (userId)->
    conn = new Y.Test userId
    super new Y conn

  type: "ObjectTest"

  getRandomRoot: (user_num, root, depth = @max_depth)->
    root ?= @users[user_num]
    if depth is 0 or _.random(0,1) is 1 # take root
      root
    else # take child
      depth--
      elems = null
      if root._name is "Object"
        elems =
          for oname,val of root.val()
            val
      else if root._name is "Array"
        elems = root.val()
      else
        return root

      elems = elems.filter (elem)->
        elem? and ((elem._name is "Array") or (elem._name is "Object"))
      if elems.length is 0
        root
      else
        p = elems[_.random(0, elems.length-1)]
        @getRandomRoot user_num, p, depth

  getGeneratingFunctions: (user_num)->
    super(user_num).concat [
        f : (y)=> # Delete Object Property
          list = for name, o of y.val()
            name
          if list.length > 0
            key = list[_.random(0,list.length-1)]
            y.delete(key)
        types: [Y.Object]
      ,
        f : (y)=> # SET Object Property
          y.val(@getRandomKey(), new Y.Object(@getRandomObject()))
        types: [Y.Object]
      ,
        f : (y)=> # SET PROPERTY TEXT
          y.val(@getRandomKey(), new Y.Text(@getRandomText()))
        types: [Y.Object]
      ,
        f : (y)=> # SET PROPERTY List
          y.val(@getRandomKey(), new Y.List(@getRandomText().split("")))
        types: [Y.Object]
      ,
        f : (y)=> # Delete Array Element
          list = y.val()
          if list.length > 0
            i = _.random(0,list.length-1)
            y.delete(i)
        types: [Y.List]
      ,
        f : (y)=> # insert Object mutable
          l = y.val().length
          y.insert(_.random(0, l-1), new Y.Object(@getRandomObject()))
        types: [Y.List]
      ,
        f : (y)=> # insert Text mutable
          l = y.val().length
          y.insert(_.random(0, l-1), new Y.Text(@getRandomText()))
        types : [Y.List]
      ,
        f : (y)=> # insert List mutable
          l = y.val().length
          y.insert(_.random(0, l-1), new Y.List(@getRandomText().split("")))
        types : [Y.List]
      ,
        f : (y)=> # insert Number (primitive object)
          l = y.val().length
          y.insert(_.random(0,l-1), _.random(0,42))
        types : [Y.List]
      ,
        f : (y)=> # SET Object Property (circular)
          y.val(@getRandomKey(), @getRandomRoot user_num)
        types: [Y.Object]
      ,
        f : (y)=> # insert Object mutable (circular)
          l = y.val().length
          y.insert(_.random(0, l-1), @getRandomRoot user_num)
        types: [Y.List]
      ,
        f : (y)=> # INSERT TEXT
          y
          pos = _.random 0, (y.val().length-1)
          y.insert pos, @getRandomText()
          null
        types: [Y.Text]
      ,
        f : (y)-> # DELETE TEXT
          if y.val().length > 0
            pos = _.random 0, (y.val().length-1) # TODO: put here also arbitrary number (test behaviour in error cases)
            length = _.random 0, (y.val().length - pos)
            ops1 = y.delete pos, length
          undefined
        types : [Y.Text]
      ]

module.exports = ObjectTest

describe "Object Test", ->
  @timeout 500000

  beforeEach (done)->
    @yTest = new ObjectTest()
    @users = @yTest.users

    @test_user = @yTest.makeNewUser "test_user"
    done()

  it "can handle many engines, many operations, concurrently (random)", ->
    console.log "" # TODO
    @yTest.run()

  it "has a working test suite", ->
    @yTest.compareAll()

  it "handles double-late-join", ->
    test = new ObjectTest("double")
    test.run()
    @yTest.run()
    u1 = test.users[0]
    u2 = @yTest.users[1]
    ops1 = u1._model.HB._encode()
    ops2 = u2._model.HB._encode()
    u1._model.engine.applyOp ops2, true
    u2._model.engine.applyOp ops1, true

    expect(@yTest.compare(u1, u2)).to.not.be.undefined

  it "can handle creaton of complex json (1)", ->
    @yTest.users[0].val('a', new Y.Text('q'))
    @yTest.users[1].val('a', new Y.Text('t'))
    @yTest.compareAll()
    q = @yTest.users[2].val('a')
    q.insert(0,'A')
    @yTest.compareAll()
    expect(@yTest.getSomeUser().val("a").val()).to.equal("At")

  it "can handle creaton of complex json (2)", ->
    @yTest.getSomeUser().val('x', new Y.Object({'a':'b'}))
    @yTest.getSomeUser().val('a', new Y.Object({'a':{q: new Y.Text("dtrndtrtdrntdrnrtdnrtdnrtdnrtdnrdnrdt")}}))
    @yTest.getSomeUser().val('b', new Y.Object({'a':{}}))
    @yTest.getSomeUser().val('c', new Y.Object({'a':'c'}))
    @yTest.getSomeUser().val('c', new Y.Object({'a':'b'}))
    @yTest.compareAll()
    q = @yTest.getSomeUser().val("a").val("a").val("q")
    q.insert(0,'A')
    @yTest.compareAll()
    expect(@yTest.getSomeUser().val("a").val("a").val("q").val()).to.equal("Adtrndtrtdrntdrnrtdnrtdnrtdnrtdnrdnrdt")

  it "can handle creaton of complex json (3)", ->
    @yTest.users[0].val('l', new Y.List([1,2,3]))
    @yTest.users[1].val('l', new Y.List([4,5,6]))
    @yTest.compareAll()
    @yTest.users[2].val('l').insert(0,'A')
    w = @yTest.users[1].val('l').insert(0,new Y.Text('B')).val(0)
    w.insert 1, "C"
    expect(w.val()).to.equal("BC")
    @yTest.compareAll()

  it "handles immutables and primitive data types", ->
    @yTest.getSomeUser().val('string', "text")
    @yTest.getSomeUser().val('number', 4)
    @yTest.getSomeUser().val('object', new Y.Object({q:"rr"}))
    @yTest.getSomeUser().val('null', null)
    @yTest.compareAll()
    expect(@yTest.getSomeUser().val('string')).to.equal "text"
    expect(@yTest.getSomeUser().val('number')).to.equal 4
    expect(@yTest.getSomeUser().val('object').val('q')).to.equal "rr"
    expect(@yTest.getSomeUser().val('null') is null).to.be.ok

  it "handles immutables and primitive data types (2)", ->
    @yTest.users[0].val('string', "text")
    @yTest.users[1].val('number', 4)
    @yTest.users[2].val('object', new Y.Object({q:"rr"}))
    @yTest.users[0].val('null', null)
    @yTest.compareAll()
    expect(@yTest.getSomeUser().val('string')).to.equal "text"
    expect(@yTest.getSomeUser().val('number')).to.equal 4
    expect(@yTest.getSomeUser().val('object').val('q')).to.equal "rr"
    expect(@yTest.getSomeUser().val('null') is null).to.be.ok

  it "Observers work on JSON Types (add type observers, local and foreign)", ->
    u = @yTest.users[0]
    @yTest.flushAll()
    last_task = null
    observer1 = (changes)->
      expect(changes.length).to.equal(1)
      change = changes[0]
      expect(change.type).to.equal("add")
      expect(change.object).to.equal(u)
      expect(change.changedBy).to.equal('0')
      expect(change.name).to.equal("newStuff")
      last_task = "observer1"
    u.observe observer1
    u.val("newStuff",new Y.Text("someStuff"))
    expect(last_task).to.equal("observer1")
    u.unobserve observer1

    observer2 = (changes)->
      expect(changes.length).to.equal(1)
      change = changes[0]
      expect(change.type).to.equal("add")
      expect(change.object).to.equal(u)
      expect(change.changedBy).to.equal('1')
      expect(change.name).to.equal("moreStuff")
      last_task = "observer2"
    u.observe observer2
    v = @yTest.users[1]
    v.val("moreStuff","someMoreStuff")
    @yTest.flushAll()
    expect(last_task).to.equal("observer2")
    u.unobserve observer2

  it "Observers work on JSON Types (update type observers, local and foreign)", ->
    u = @yTest.users[0].val("newStuff", new Y.Text("oldStuff")).val("moreStuff",new Y.Text("moreOldStuff"))
    @yTest.flushAll()
    last_task = null
    observer1 = (changes)->
      expect(changes.length).to.equal(1)
      change = changes[0]
      expect(change.type).to.equal("update")
      expect(change.object).to.equal(u)
      expect(change.changedBy).to.equal('0')
      expect(change.name).to.equal("newStuff")
      expect(change.oldValue.val()).to.equal("oldStuff")
      last_task = "observer1"
    u.observe observer1
    u.val("newStuff","someStuff")
    expect(last_task).to.equal("observer1")
    u.unobserve observer1

    observer2 = (changes)->
      expect(changes.length).to.equal(1)
      change = changes[0]
      expect(change.type).to.equal("update")
      expect(change.object).to.equal(u)
      expect(change.changedBy).to.equal('1')
      expect(change.name).to.equal("moreStuff")
      expect(change.oldValue.val()).to.equal("moreOldStuff")
      last_task = "observer2"
    u.observe observer2
    v = @yTest.users[1]
    v.val("moreStuff","someMoreStuff")
    @yTest.flushAll()
    expect(last_task).to.equal("observer2")
    u.unobserve observer2


  it "Observers work on JSON Types (delete type observers, local and foreign)", ->
    u = @yTest.users[0].val("newStuff",new Y.Text("oldStuff")).val("moreStuff",new Y.Text("moreOldStuff"))
    @yTest.flushAll()
    last_task = null
    observer1 = (changes)->
      expect(changes.length).to.equal(1)
      change = changes[0]
      expect(change.type).to.equal("delete")
      expect(change.object).to.equal(u)
      expect(change.changedBy).to.equal('0')
      expect(change.name).to.equal("newStuff")
      expect(change.oldValue.val()).to.equal("oldStuff")
      last_task = "observer1"
    u.observe observer1
    u.delete("newStuff")
    expect(last_task).to.equal("observer1")
    u.unobserve observer1

    observer2 = (changes)->
      expect(changes.length).to.equal(1)
      change = changes[0]
      expect(change.type).to.equal("delete")
      expect(change.object).to.equal(u)
      expect(change.changedBy).to.equal('1')
      expect(change.name).to.equal("moreStuff")
      expect(change.oldValue.val()).to.equal("moreOldStuff")
      last_task = "observer2"
    u.observe observer2
    v = @yTest.users[1]
    v.delete("moreStuff")
    @yTest.flushAll()
    expect(last_task).to.equal("observer2")
    u.unobserve observer2

  it "can handle circular JSON", ->
    u = @yTest.users[0]
    u.val("me", u)
    @yTest.compareAll()
    u.val("stuff", new Y.Object({x: true}))
    u.val("same_stuff", u.val("stuff"))
    u.val("same_stuff").val("x", 5)
    expect(u.val("same_stuff").val("x")).to.equal(5)
    @yTest.compareAll()
    u.val("stuff").val("y", u.val("stuff"))
    @yTest.compareAll()



