Actions = require '../../src/flux/actions'
TaskQueue = require '../../src/flux/stores/task-queue'
Task = require '../../src/flux/tasks/task'

{isTempId} = require '../../src/flux/models/utils'

{APIError,
 OfflineError,
 TimeoutError} = require '../../src/flux/errors'

class TaskSubclassA extends Task
  constructor: (val) -> @aProp = val # forgot to call super

class TaskSubclassB extends Task
  constructor: (val) -> @bProp = val; super

describe "TaskQueue", ->
  makeUnstartedTask = (task) ->
    TaskQueue._initializeTask(task)
    return task

  makeLocalStarted = (task) ->
    TaskQueue._initializeTask(task)
    task.queueState.isProcessing = true
    return task

  makeLocalFailed = (task) ->
    TaskQueue._initializeTask(task)
    task.queueState.performedLocal = Date.now()
    return task

  makeRemoteStarted = (task) ->
    TaskQueue._initializeTask(task)
    task.queueState.isProcessing = true
    task.queueState.remoteAttempts = 1
    task.queueState.performedLocal = Date.now()
    return task

  makeRemoteSuccess = (task) ->
    TaskQueue._initializeTask(task)
    task.queueState.remoteAttempts = 1
    task.queueState.performedLocal = Date.now()
    task.queueState.performedRemote = Date.now()
    return task

  makeRemoteFailed = (task) ->
    TaskQueue._initializeTask(task)
    task.queueState.remoteAttempts = 1
    task.queueState.performedLocal = Date.now()
    return task

  beforeEach ->
    TaskQueue._onlineStatus = true
    @task              = new Task()
    @unstartedTask     = makeUnstartedTask(new Task())
    @localStarted      = makeLocalStarted(new Task())
    @localFailed       = makeLocalFailed(new Task())
    @remoteStarted     = makeRemoteStarted(new Task())
    @remoteSuccess     = makeRemoteSuccess(new Task())
    @remoteFailed      = makeRemoteFailed(new Task())

  unstartedTask = (task) ->
    taks.queueState.shouldRetry = false
    taks.queueState.isProcessing = false
    taks.queueState.remoteAttempts = 0
    taks.queueState.perfomredLocal = false
    taks.queueState.performedRemote = false
    taks.queueState.notifiedOffline = false

  startedTask = (task) ->
    taks.queueState.shouldRetry = false
    taks.queueState.isProcessing = true
    taks.queueState.remoteAttempts = 0
    taks.queueState.perfomredLocal = false
    taks.queueState.performedRemote = false
    taks.queueState.notifiedOffline = false

  localTask = (task) ->
    taks.queueState.shouldRetry = false
    taks.queueState.isProcessing = true
    taks.queueState.remoteAttempts = 0
    taks.queueState.perfomredLocal = false
    taks.queueState.performedRemote = false
    taks.queueState.notifiedOffline = false

  afterEach ->
    TaskQueue._queue = []
    TaskQueue._completed = []

  describe "enqueue", ->
    it "makes sure you've queued a real task", ->
      expect( -> TaskQueue.enqueue("asamw")).toThrow()

    it "adds it to the queue", ->
      TaskQueue.enqueue(@task)
      expect(TaskQueue._queue.length).toBe 1

    it "notifies the queue should be processed", ->
      spyOn(TaskQueue, "_processTask")
      spyOn(TaskQueue, "_processQueue").andCallThrough()

      TaskQueue.enqueue(@task)

      expect(TaskQueue._processQueue).toHaveBeenCalled()
      expect(TaskQueue._processTask).toHaveBeenCalledWith(@task)
      expect(TaskQueue._processTask.calls.length).toBe 1

    it "ensures all tasks have an id", ->
      TaskQueue.enqueue(new TaskSubclassA())
      TaskQueue.enqueue(new TaskSubclassB())
      expect(isTempId(TaskQueue._queue[0].id)).toBe true
      expect(isTempId(TaskQueue._queue[1].id)).toBe true

    it "dequeues Obsolete tasks", ->
      class KillsTaskA extends Task
        constructor: ->
        shouldDequeueOtherTask: (other) -> other instanceof TaskSubclassA

      taskToDie = makeRemoteFailed(new TaskSubclassA())

      spyOn(TaskQueue, "dequeue").andCallThrough()

      TaskQueue._queue = [taskToDie, @remoteFailed]
      TaskQueue.enqueue(new KillsTaskA())

      expect(TaskQueue._queue.length).toBe 2
      expect(TaskQueue.dequeue).toHaveBeenCalledWith(taskToDie, silent: true)
      expect(TaskQueue.dequeue.calls.length).toBe 1

  describe "dequeue", ->
    beforeEach ->
      TaskQueue._queue = [@unstartedTask,
                          @localStarted,
                          @remoteStarted,
                          @remoteFailed]

    it "grabs the task by object", ->
      found = TaskQueue._parseArgs(@remoteStarted)
      expect(found).toBe @remoteStarted

    it "grabs the task by id", ->
      found = TaskQueue._parseArgs(@remoteStarted.id)
      expect(found).toBe @remoteStarted

    it "throws an error if the task isn't found", ->
      expect( -> TaskQueue.dequeue("bad")).toThrow()

    it "calls cleanup on dequeued tasks", ->
      spyOn(@remoteStarted, "cleanup")
      TaskQueue.dequeue(@remoteStarted, silent: true)
      expect(@remoteStarted.cleanup).toHaveBeenCalled()

    it "moves it from the queue", ->
      TaskQueue.dequeue(@remoteStarted, silent: true)
      expect(TaskQueue._queue.length).toBe 3
      expect(TaskQueue._completed.length).toBe 1

    it "marks it as no longer processing", ->
      TaskQueue.dequeue(@remoteStarted, silent: true)
      expect(@remoteStarted.queueState.isProcessing).toBe false

    it "notifies the queue has been updated", ->
      spyOn(TaskQueue, "_processQueue")

      TaskQueue.dequeue(@remoteStarted)

      expect(TaskQueue._processQueue).toHaveBeenCalled()
      expect(TaskQueue._processQueue.calls.length).toBe 1

  describe "process Task", ->
    it "doesn't process processing tasks", ->
      spyOn(@remoteStarted, "performLocal")
      spyOn(@remoteStarted, "performRemote")
      TaskQueue._processTask(@remoteStarted)
      expect(@remoteStarted.performLocal).not.toHaveBeenCalled()
      expect(@remoteStarted.performRemote).not.toHaveBeenCalled()

    it "doesn't process blocked tasks", ->
      class BlockedByTaskA extends Task
        constructor: ->
        shouldWaitForTask: (other) -> other instanceof TaskSubclassA

      blockedByTask = new BlockedByTaskA()
      spyOn(blockedByTask, "performLocal")
      spyOn(blockedByTask, "performRemote")

      blockingTask = makeRemoteFailed(new TaskSubclassA())

      TaskQueue._queue = [blockingTask, @remoteFailed]
      TaskQueue.enqueue(blockedByTask)

      expect(TaskQueue._queue.length).toBe 3
      expect(blockedByTask.performLocal).not.toHaveBeenCalled()
      expect(blockedByTask.performRemote).not.toHaveBeenCalled()

    it "doesn't block itself", ->
      class BlockingTask extends Task
        constructor: ->
        shouldWaitForTask: (other) -> other instanceof BlockingTask

      blockedByTask = new BlockingTask()
      spyOn(blockedByTask, "performLocal")
      spyOn(blockedByTask, "performRemote")

      blockingTask = makeRemoteFailed(new BlockingTask())

      TaskQueue._queue = [blockingTask, @remoteFailed]
      TaskQueue.enqueue(blockedByTask)

      expect(TaskQueue._queue.length).toBe 3
      expect(blockedByTask.performLocal).not.toHaveBeenCalled()
      expect(blockedByTask.performRemote).not.toHaveBeenCalled()

    it "sets the processing bit", ->
      spyOn(@unstartedTask, "performLocal").andCallFake -> Promise.resolve()
      TaskQueue._processTask(@unstartedTask)
      expect(@unstartedTask.queueState.isProcessing).toBe true

    it "performs local if it's a fresh task", ->
      spyOn(@unstartedTask, "performLocal").andCallFake -> Promise.resolve()
      TaskQueue._processTask(@unstartedTask)
      expect(@unstartedTask.performLocal).toHaveBeenCalled()

  describe "performLocal", ->
    it "on success it marks it as complete with the timestamp", ->
      spyOn(@unstartedTask, "performLocal").andCallFake -> Promise.resolve()
      spyOn(@unstartedTask, "performRemote").andCallFake -> Promise.resolve()
      runs ->
        TaskQueue.enqueue(@unstartedTask)
      waitsFor =>
        @unstartedTask.queueState.performedLocal isnt false
      runs ->
        expect(@unstartedTask.queueState.performedLocal).toBeGreaterThan 0

    it "throws an error if it fails", ->
      spyOn(@unstartedTask, "performLocal").andCallFake -> Promise.reject("boo")
      spyOn(@unstartedTask, "performRemote").andCallFake -> Promise.resolve()
      runs ->
        TaskQueue.enqueue(@unstartedTask)
      waitsFor =>
        @unstartedTask.queueState.isProcessing == false
      runs ->
        expect(@unstartedTask.queueState.localError).toBe "boo"
        expect(@unstartedTask.performLocal).toHaveBeenCalled()
        expect(@unstartedTask.performRemote).not.toHaveBeenCalled()

    it "dequeues the task if it fails locally", ->
      spyOn(@unstartedTask, "performLocal").andCallFake -> Promise.reject("boo")
      spyOn(@unstartedTask, "performRemote").andCallFake -> Promise.resolve()
      runs ->
        TaskQueue.enqueue(@unstartedTask)
      waitsFor =>
        @unstartedTask.queueState.isProcessing == false
      runs ->
        expect(TaskQueue._queue.length).toBe 0
        expect(TaskQueue._completed.length).toBe 1

  describe "performRemote", ->
    beforeEach ->
      spyOn(@unstartedTask, "performLocal").andCallFake -> Promise.resolve()

    it "performs remote properly", ->
      spyOn(@unstartedTask, "performRemote").andCallFake -> Promise.resolve()
      runs ->
        TaskQueue.enqueue(@unstartedTask)
      waitsFor =>
        @unstartedTask.queueState.performedRemote isnt false
      runs ->
        expect(@unstartedTask.performLocal).toHaveBeenCalled()
        expect(@unstartedTask.performRemote).toHaveBeenCalled()

    it "dequeues on success", ->
      spyOn(@unstartedTask, "performRemote").andCallFake -> Promise.resolve()
      runs ->
        TaskQueue.enqueue(@unstartedTask)
      waitsFor =>
        @unstartedTask.queueState.isProcessing is false and
        @unstartedTask.queueState.performedRemote > 0
      runs ->
        expect(TaskQueue._queue.length).toBe 0
        expect(TaskQueue._completed.length).toBe 1

    it "notifies we're offline the first time", ->
      spyOn(TaskQueue, "_isOnline").andReturn false
      spyOn(@unstartedTask, "performRemote").andCallFake -> Promise.resolve()
      spyOn(@unstartedTask, "onError")
      runs ->
        TaskQueue.enqueue(@unstartedTask)
      waitsFor =>
        @unstartedTask.queueState.notifiedOffline == true
      runs ->
        expect(@unstartedTask.performLocal).toHaveBeenCalled()
        expect(@unstartedTask.performRemote).not.toHaveBeenCalled()
        expect(@unstartedTask.onError).toHaveBeenCalled()
        expect(@unstartedTask.queueState.isProcessing).toBe false
        expect(@unstartedTask.onError.calls[0].args[0] instanceof OfflineError).toBe true

    it "doesn't notify we're offline the second+ time", ->
      spyOn(TaskQueue, "_isOnline").andReturn false
      spyOn(@remoteFailed, "performLocal").andCallFake -> Promise.resolve()
      spyOn(@remoteFailed, "performRemote").andCallFake -> Promise.resolve()
      spyOn(@remoteFailed, "onError")
      @remoteFailed.queueState.notifiedOffline = true
      TaskQueue._queue = [@remoteFailed]
      runs ->
        TaskQueue._processQueue()
      waitsFor =>
        @remoteFailed.queueState.isProcessing is false
      runs ->
        expect(@remoteFailed.performLocal).not.toHaveBeenCalled()
        expect(@remoteFailed.performRemote).not.toHaveBeenCalled()
        expect(@remoteFailed.onError).not.toHaveBeenCalled()

    it "marks performedRemote on success", ->
      spyOn(@unstartedTask, "performRemote").andCallFake -> Promise.resolve()
      runs ->
        TaskQueue.enqueue(@unstartedTask)
      waitsFor =>
        @unstartedTask.queueState.performedRemote isnt false
      runs ->
        expect(@unstartedTask.queueState.performedRemote).toBeGreaterThan 0

    it "on failure it notifies of the error", ->
      err = new APIError
      spyOn(@unstartedTask, "performRemote").andCallFake -> Promise.reject(err)
      spyOn(@unstartedTask, "onError")
      runs ->
        TaskQueue.enqueue(@unstartedTask)
      waitsFor =>
        @unstartedTask.queueState.isProcessing is false
      runs ->
        expect(@unstartedTask.performLocal).toHaveBeenCalled()
        expect(@unstartedTask.performRemote).toHaveBeenCalled()
        expect(@unstartedTask.onError).toHaveBeenCalledWith(err)

    it "dequeues on failure", ->
      err = new APIError
      spyOn(@unstartedTask, "performRemote").andCallFake -> Promise.reject(err)
      runs ->
        TaskQueue.enqueue(@unstartedTask)
      waitsFor =>
        @unstartedTask.queueState.isProcessing is false
      runs ->
        expect(TaskQueue._queue.length).toBe 0
        expect(TaskQueue._completed.length).toBe 1

    it "on failure it sets the appropriate bits", ->
      err = new APIError
      spyOn(@unstartedTask, "performRemote").andCallFake -> Promise.reject(err)
      spyOn(@unstartedTask, "onError")
      runs ->
        TaskQueue.enqueue(@unstartedTask)
      waitsFor =>
        @unstartedTask.queueState.isProcessing is false
      runs ->
        expect(@unstartedTask.queueState.notifiedOffline).toBe false
        expect(@unstartedTask.queueState.remoteError).toBe err

  describe "under stress", ->
    beforeEach ->
      TaskQueue._queue = [@unstartedTask,
                          @remoteFailed]
    it "when all tasks pass it processes all items", ->
      for task in TaskQueue._queue
        spyOn(task, "performLocal").andCallFake -> Promise.resolve()
        spyOn(task, "performRemote").andCallFake -> Promise.resolve()
      runs ->
        TaskQueue.enqueue(new Task)
      waitsFor ->
        TaskQueue._queue.length is 0
      runs ->
        expect(TaskQueue._completed.length).toBe 3
