chai = require 'chai'
should = chai.should()
sinon = require 'sinon'
chai.use require('sinon-chai')

describe 'OptionsSync', ->
  OptionsSync = require '../src/options_sync'
  Storage = require '../src/storage'
  Log = require '../src/log'
  Promise = require 'bluebird'

  before ->
    # Silence storage and sync logging.
    sinon.stub(Log, 'log')

  after ->
    Log.log.restore()

  # coffeelint: disable=missing_fat_arrows
  hookPostBasic = (func, hook) -> ->
    result = func.apply(this, arguments)
    hook.apply(this, arguments)
    return result
  # coffeelint: enable=missing_fat_arrows

  hookPost = (args...) ->
    if args.length == 2
      [func, hook] = args
      hostPostBasic(func, hook)
    else
      [obj, method, hook] = args
      obj[method] = hookPostBasic(obj[method], hook)

  describe '#merge', ->
    sync = new OptionsSync()
    it 'should choose the one with newer revision', ->
      newVal = {revision: '2'}
      oldVal = {revision: '1'}
      sync.merge('example', newVal, oldVal).should.equal(newVal)
    it 'should use oldVal when sync is disabled in newVal', ->
      newVal = {revision: '2', is: 'newVal', syncOptions: 'disabled'}
      oldVal = {revision: '1', is: 'oldVal'}
      sync.merge('example', newVal, oldVal).should.equal(oldVal)
    it 'should use oldVal when sync is disabled in oldVal', ->
      newVal = {revision: '2', is: 'newVal'}
      oldVal = {revision: '1', is: 'oldVal', syncOptions: 'disabled'}
      sync.merge('example', newVal, oldVal).should.equal(oldVal)
    it 'should favor oldVal when revisions are equal', ->
      newVal = {revision: '1', is: 'newVal'}
      oldVal = {revision: '1', is: 'oldVal'}
      sync.merge('example', newVal, oldVal).should.equal(oldVal)
    it 'should favor oldVal when newVal deeply equals oldVal', ->
      newVal = {they: 'are', the: 'same'}
      oldVal = {they: 'are', the: 'same'}
      sync.merge('example', newVal, oldVal).should.equal(oldVal)
    it 'should choose newVal when newVal is different', ->
      newVal = {they: 'are', not: 'equal'}
      oldVal = {they: 'are', not: 'identical'}
      sync.merge('example', newVal, oldVal).should.equal(newVal)

  describe '#requestPush', ->
    unlimited = new OptionsSync.TokenBucket()

    it 'should store pendingChanges', ->
      sync = new OptionsSync()
      sync.enabled = false
      sync.requestPush({a: 1})
      sync.pendingChanges().should.eql({a: 1})
    it 'should schedule storage write', (done) ->
      check = ->
        return if storage.set.callCount == 0 or storage.remove.callCount == 0
        storage.set.should.have.been.calledOnce.and.calledWith({b: 1})
        storage.remove.should.have.been.calledOnce.and.calledWith(['a'])
        done()

      storage = new Storage()
      storage.set({a: 1})
      hookPost storage, 'set', check
      hookPost storage, 'remove', check

      sinon.spy(storage, 'set')
      sinon.spy(storage, 'remove')

      sync = new OptionsSync(storage, unlimited)
      sync.debounce = 0
      sync.requestPush({a: undefined, b: 1})

    it 'should combine multiple write operations', (done) ->
      check = ->
        return if storage.set.callCount == 0 or storage.remove.callCount == 0
        storage.set.should.have.been.calledOnce.and.calledWith({c: 1, d: 1})
        storage.remove.should.have.been.calledOnce.and.calledWith(['a', 'b'])
        done()

      storage = new Storage()
      storage.set({a: 1, b: 1})
      hookPost storage, 'set', check
      hookPost storage, 'remove', check

      sinon.spy(storage, 'set')
      sinon.spy(storage, 'remove')

      sync = new OptionsSync(storage, unlimited)
      sync.debounce = 0
      sync.requestPush({a: undefined})
      sync.requestPush({b: 2})
      sync.requestPush({b: undefined})
      sync.requestPush({c: 1})
      sync.requestPush({d: 1})
      sync.requestPush({e: 1})
      sync.requestPush({e: undefined})

    it 'should disable syncing for the profiles if quota is exceeded', (done) ->
      options = {'+a': {is: 'a', oversized: true}, b: {is: 'b'}}

      storage = new Storage()
      storage.set = (changes) ->
        for key, value of changes
          if value.oversized
            err = new Storage.QuotaExceededError()
            err.perItem = true
            return Promise.reject(err)
        storage.set.should.have.been.calledTwice
        storage.set.should.have.been.calledWith(options)
        storage.set.should.have.been.calledWith({b: {is: 'b'}})
        options['+a'].syncOptions.should.equal('disabled')
        options['+a'].syncError.reason.should.equal('quotaPerItem')
        done()
        Promise.resolve()

      sinon.spy(storage, 'set')

      sync = new OptionsSync(storage, unlimited)
      sync.debounce = 0
      sync.requestPush(options)

  describe '#copyTo', ->
    it 'should fetch all items from remote storage', (done) ->
      remote = new Storage()
      remote.set({a: 1, b: 2, c: 3})

      storage = new Storage()
      hookPost storage, 'set', ->
        storage.set.should.have.been.calledOnce.and.calledWith(
          {a: 1, b: 2, c: 3}
        )
        done()

      sinon.spy(storage, 'set')

      sync = new OptionsSync(remote)
      sync.copyTo(storage)

    it 'should merge with local as base', (done) ->
      check = ->
        return if storage.set.callCount == 0 or storage.remove.callCount == 0
        storage.set.should.have.been.calledOnce.and.calledWith({b: 2, c: 3})
        storage.remove.should.have.been.calledOnce.and.calledWith(['d'])
        done()

      remote = new Storage()
      remote.set({a: 1, b: 2, c: 3, d: undefined})

      storage = new Storage()
      storage.set({a: 1, b: 0, d: 4})

      hookPost storage, 'set', check
      hookPost storage, 'remove', check

      sinon.spy(storage, 'set')
      sinon.spy(storage, 'remove')

      sync = new OptionsSync(remote)
      sync.copyTo(storage)

  describe '#watchAndPull', ->
    it 'should pull changes into local when remote changes', (done) ->
      check = ->
        return if storage.set.callCount == 0 or storage.remove.callCount == 0
        remote.watch.should.have.been.calledOnce
        storage.set.should.have.been.calledOnce.and.calledWith({b: 2, c: 3})
        storage.remove.should.have.been.calledOnce.and.calledWith(['d'])
        done()

      remote = new Storage()
      hookPost remote, 'watch', (_, callback) ->
        setTimeout (->
          callback({a: 1})
          callback({b: 2})
          callback({c: 3})
          callback({d: undefined})
        ), 10

      sinon.spy(remote, 'watch')

      storage = new Storage()
      storage.set({a: 1, b: 0, d: 4})

      hookPost storage, 'set', check
      hookPost storage, 'remove', check

      sinon.spy(storage, 'set')
      sinon.spy(storage, 'remove')

      sync = new OptionsSync(remote)
      sync.pullThrottle = 0
      sync.watchAndPull(storage)