/*jshint node: true, indent:2, loopfunc: true, asi: true, undef:true, mocha: true */

// require('longjohn')

var sync   = require('../sync')
var Fiber = require('fibers')
var expect = require('chai').expect

describe('Control Flow', function(){
  var waitAndReturn = function(time, err, value, cb){
    setTimeout(function(){cb(err, value)}, time)
  }

  var fn = function(arg, cb){
    expect(arg).to.eql('something')
    process.nextTick(function(){
      cb(null, 'ok')
    })
  }

  it('should provide await & defer', function(done){
    sync.fiber(function(){
      var result = sync.await(fn('something', sync.defer()))
      expect(result).to.eql('ok')
    }, done)
  })

  it('should synchronize function', function(done){
    fn = sync(fn)
    sync.fiber(function(){
      expect(fn('something')).to.eql('ok')
    }, done)
  })

  it('should be save aginst synchronizing function twice', function(done){
    fn = sync(sync(fn))
    sync.fiber(function(){
      expect(fn('something')).to.eql('ok')
    }, done)
  })

  it('should allow call synchronized function explicitly', function(done){
    fn = sync(fn)
    fn('something', function(err, result){
      expect(result).to.eql('ok')
      done(err)
    })
  })

  it("should catch asynchronous errors", function(done){
    var fn = function(cb){
      process.nextTick(function(){
        cb(new Error('an error'))
      })
    }
    fn = sync(fn)
    sync.fiber(function(){
      var err
      try {
        fn()
      } catch (e) {
        err = e
      }
      expect(err.message).to.eql('an error')
    }, done)
  })

  it("should be compatible with not asynchronous cbs", function(done){
    fn = function(cb){
      cb(null, 'ok')
    }
    fn = sync(fn)
    sync.fiber(function(){
      expect(fn()).to.eql('ok')
    }, done)
  })

  it("should catch non asynchronous errors", function(done){
    fn = function(cb){
      cb(new Error('an error'))
    }
    fn = sync(fn)
    sync.fiber(function(){
      var err
      try {
        fn()
      } catch (e) {
        err = e
      }
      expect(err.message).to.eql('an error')
    }, done)
  })

  describe("Special cases", function(){
    it('should be able to emulate sleep', function(done){
      var sleep = function(ms){
        sync.await(setTimeout(sync.defer(), ms))
      }

      sync.fiber(function(){
        var start = new Date().getTime()
        sleep(50)
        expect(new Date().getTime()).to.be.greaterThan(start)
      }, done)
    })
  })

  it('should support parallel calls', function(done){
    sync.fiber(function(){
      var calls = []
      var readA = function(cb){
        calls.push('readA')
        process.nextTick(function(){
          calls.push('nextTick')
          cb(null, 'dataA')
        })
      }
      var readB = function(cb){
        calls.push('readB')
        process.nextTick(function(){
          calls.push('nextTick')
          cb(null, 'dataB')
        })
      }

      sync.parallel(function(){
        readA(sync.defer())
        readB(sync.defer())
      })
      var results = sync.await()

      expect(results).to.eql(['dataA', 'dataB'])
      expect(calls).to.eql(['readA', 'readB', 'nextTick', 'nextTick'])
    }, done)
  })

  it('should support multiple arguments', function(done){
    sync.fiber(function(){
      var read = function(cb){
        process.nextTick(function(){
          cb(null, 'data1', 'data2')
        })
      }

      var result = sync.await(read(sync.defers()))
      expect(result).to.eql(['data1', 'data2'])
    }, done)
  })

  it('should support multiple `named` arguments', function(done){
    sync.fiber(function(){
      var read = function(cb){
        process.nextTick(function(){
          cb(null, 'data1', 'data2')
        })
      }

      var result = sync.await(read(sync.defers('a', 'b')))
      expect(result).to.eql({a: 'data1', b: 'data2'})
    }, done)
  })

  it('should support multiple arguments parallel calls', function(done){
    sync.fiber(function(){
      var read = function(cb){
        process.nextTick(function(){
          cb(null, 'data1', 'data2')
        })
      }

      sync.parallel(function(){
        read(sync.defers())
        read(sync.defers('a', 'b'))
      })
      var results = sync.await()
      expect(results).to.eql([['data1', 'data2'], {a: 'data1', b: 'data2'}])
    }, done)
  })

  it('should fiber call just once at raise error', function(done){
    var read = function(cb){
      process.nextTick(function(){
        cb(new Error('an error'))
      })
    }

    var called = 0
    sync.fiber(function(){
      called += 1
      sync.await(read(sync.defers()))
    }, function() {})

    setTimeout(function(){
      expect(called).to.eql(1)
      done()
    }, 100)
  })

  it('should handle parallel errors', function(done){
    sync.fiber(function(){
      var calls = []
      var readA = function(cb){
        process.nextTick(function(){
          cb(new Error("error a"))
        })
      }
      var readB = function(cb){
        process.nextTick(function(){
          cb(new Error("error b"))
        })
      }

      try {
        sync.parallel(function(){
          readA(sync.defer())
          readB(sync.defer())
        })
        var results = sync.await()
      }catch(err){
        if(err.message == 'error a') expect(err.message).to.eql('error a')
        else expect(err.message).to.eql('error b')
      }
    }, done)
  })

  it('should handle parallel errors with multiple arguments', function(done){
    sync.fiber(function(){
      var calls = []
      var readA = function(cb){
        process.nextTick(function(){
          cb(new Error("error a"))
        })
      }
      var readB = function(cb){
        process.nextTick(function(){
          cb(new Error("error b"))
        })
      }

      try {
        sync.parallel(function(){
          readA(sync.defers())
          readB(sync.defers())
        })
        var results = sync.await()
      }catch(err){
        if(err.message == 'error a') expect(err.message).to.eql('error a')
        else expect(err.message).to.eql('error b')
      }
    }, done)
  })

  it('should not unwind when await is called after an empty parallel block', function(done){
    sync.fiber(function(){
      sync.parallel(function(){
        // Imagine that the user intends to enumerate an array here, calling
        // `defer` once per array item, but the array is empty.
      })
      expect(sync.await()).to.eql([])
    }, done)
  })

  it('should return result from fiber', function(done){
    sync.fiber(function(){
      return 'some value'
    }, function(err, result){
      expect(err).to.eql(null)
      expect(result).to.eql('some value')
      done()
    })
  })

  it('should abort on timeout', function(done){
    sync.fiber(function(){
      waitAndReturn(10, null, 'some result', sync.deferWithTimeout(100))
      expect(sync.await()).to.eql('some result')

      try{
        waitAndReturn(10, null, 'some result', sync.deferWithTimeout(1))
        expect(sync.await()).to.eql('some result')
      }catch(err){
        expect(err.message).to.eql('defer timed out!')
      }
    }, done)
  })

  // TODO, add also the same specs for `defers`.
  it('should not resume terminated fiber with value', function(done){
    var runCount = 0
    var results = []
    sync.fiber(function(){
      runCount += 1
      waitAndReturn(1, null, 'some value', sync.defer())
    }, function(err){
      results.push(err || null)
    })

    // Need to wait for some time after the fiber ends its execution to make sure
    // it won't be runned one more time.
    setTimeout(function(){
      expect(runCount).to.eql(1)
      expect(results.length).to.eql(1)
      expect(results[0]).to.eql(null)
      done()
    }, 10)
  })

  // TODO, add also the same specs for `defers`.
  it('should not resume terminated fiber with error', function(done){
    var runCount = 0
    var results = []
    sync.fiber(function(){
      runCount += 1
      waitAndReturn(1, (new Error('some error')), null, sync.defer())
    }, function(err){
      results.push(err)
    })

    // Need to wait for some time after the fiber ends its execution to make sure
    // it won't be runned one more time.
    setTimeout(function(){
      expect(runCount).to.eql(1)
      expect(results.length).to.eql(1)
      expect(results[0]).to.eql(null)
      done()
    }, 10)
  })

  it('should call terminate callback just once', function(done) {
    var callCount = 0
    var callback = function(error) {
      callCount += 1
      throw new Error('some error')
    }
    expect(function() {sync.fiber(function() {}, callback)}).to.throw(Error)
    setTimeout(function() {
      expect(callCount).to.eql(1)
      done()
    }, 10)
  })

  it('should throw error if defer called twice', function(done){
    sync.fiber(function(){
      var defer = sync.defer()
      defer()
      sync.await()
      expect(defer).to.throw(Error)
    }, done)
  })

  it('should call defer only once in the fiber process', function(done){
    var broken = function(cb) {
      sync.fiber(function() {
        cb()
      }, cb)
    }
    sync.fiber(function(){
      broken(sync.defer())
      sync.await()
      throw new Error('an error')
    }, function(err) {
      expect(err).to.exist
      expect(err.message).to.eql("defer can't be used twice!")
      done()
    })
  })

  it('should throw error if defers called twice', function(done){
    sync.fiber(function(){
      var defer = sync.defers()
      defer()
      sync.await()
      expect(defer).to.throw(Error)
    }, done)
  })

  it('should prevent defers call just once in fiber process', function(done){
    var broken = function(cb) {
      sync.fiber(function() {
        cb()
      }, cb)
    }
    sync.fiber(function(){
      broken(sync.defers())
      sync.await()
      throw new Error('an error')
    }, function(err) {
      expect(err).to.exist
      expect(err.message).to.eql("defer can't be used twice!")
      done()
    })
  })

  it('should prevent restart fiber', function(done){
    var currentFiber
    var called = 0
    sync.fiber(function(){
      called += 1
      currentFiber = sync.Fiber.current
    })
    setTimeout(function() {
      currentFiber.run()
    }, 1)
    setTimeout(function() {
      expect(called).to.eql(1)
      done()
    }, 10);
  })

  it('should throw error when not matched defer-await pair', function(done){
    sync.fiber(function(){
	    process.nextTick(sync.defer());
      expect(function() { process.nextTick(sync.defer()) }).to.throw(Error)
      sync.await()
	    process.nextTick(sync.defers());
      expect(function() { process.nextTick(sync.defers()) }).to.throw(Error)
      sync.await()
    }, done)
  })

  beforeEach(function(){
    this.someKey = 'some value'
  })
  it('should provide asyncIt helper for tests', sync.asyncIt(function(){
    expect(Fiber.current).to.exist
    expect(this.someKey).to.eql('some value')
  }))
})