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; super

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

describe "TaskQueue", ->

  makeUnstartedTask = (task) ->
    task

  makeProcessing = (task) ->
    task.queueState.isProcessing = true
    task

  beforeEach ->
    @task              = new Task()
    @unstartedTask     = makeUnstartedTask(new Task())
    @processingTask    = makeProcessing(new Task())

  afterEach ->
    # Flush any throttled or debounced updates
    advanceClock(1000)

  describe "findTask", ->
    beforeEach ->
      @subclassA = new TaskSubclassA()
      @subclassB1 = new TaskSubclassB("B1")
      @subclassB2 = new TaskSubclassB("B2")
      TaskQueue._queue = [@subclassA, @subclassB1, @subclassB2]

    it "accepts type as a string", ->
      expect(TaskQueue.findTask('TaskSubclassB', {bProp: 'B1'})).toEqual(@subclassB1)

    it "accepts type as a class", ->
      expect(TaskQueue.findTask(TaskSubclassB, {bProp: 'B1'})).toEqual(@subclassB1)

    it "works without a set of match criteria", ->
      expect(TaskQueue.findTask(TaskSubclassA)).toEqual(@subclassA)

    it "only returns a task that matches the criteria", ->
      expect(TaskQueue.findTask(TaskSubclassB, {bProp: 'B1'})).toEqual(@subclassB1)
      expect(TaskQueue.findTask(TaskSubclassB, {bProp: 'B2'})).toEqual(@subclassB2)
      expect(TaskQueue.findTask(TaskSubclassB, {bProp: 'B3'})).toEqual(null)

  describe "enqueue", ->
    beforeEach ->
      spyOn(@unstartedTask, 'runLocal').andCallFake =>
        @unstartedTask.queueState.localComplete = true
        Promise.resolve()

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

    it "adds it to the queue", ->
      spyOn(TaskQueue, '_processQueue').andCallFake ->
      TaskQueue.enqueue(@unstartedTask)
      advanceClock()
      expect(TaskQueue._queue.length).toBe(1)

    it "immediately calls runLocal", ->
      TaskQueue.enqueue(@unstartedTask)
      expect(@unstartedTask.runLocal).toHaveBeenCalled()

    it "add it to the queue after `performLocalComplete` has run", ->
      task = new Task()
      spyOn(atom, "isMainWindow").andReturn true
      waitsForPromise ->
        TaskQueue.enqueue(task)
        task.waitForPerformLocal().then ->
          expect(TaskQueue._queue.length).toBe 1
          expect(TaskQueue._queue[0]).toBe task

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

      TaskQueue.enqueue(@unstartedTask)
      advanceClock()
      expect(TaskQueue._processQueue).toHaveBeenCalled()
      expect(TaskQueue._processTask).toHaveBeenCalledWith(@unstartedTask)
      expect(TaskQueue._processTask.calls.length).toBe(1)

    it "throws an exception if the task does not have a queueState", ->
      task = new TaskSubclassA()
      task.queueState = undefined
      expect( => TaskQueue.enqueue(task)).toThrow()

    it "throws an exception if the task does not have an ID", ->
      task = new TaskSubclassA()
      task.id = undefined
      expect( => TaskQueue.enqueue(task)).toThrow()

    it "dequeues obsolete tasks", ->
      task = new TaskSubclassA()
      spyOn(TaskQueue, '_dequeueObsoleteTasks').andCallFake ->
      TaskQueue.enqueue(task)
      expect(TaskQueue._dequeueObsoleteTasks).toHaveBeenCalled()

  describe "_dequeueObsoleteTasks", ->
    it "should dequeue tasks based on `shouldDequeueOtherTask`", ->
      class KillsTaskA extends Task
        shouldDequeueOtherTask: (other) -> other instanceof TaskSubclassA
        performRemote: -> new Promise (resolve, reject) ->

      otherTask = new Task()
      otherTask.queueState.localComplete = true
      obsoleteTask = new TaskSubclassA()
      obsoleteTask.queueState.localComplete = true
      replacementTask = new KillsTaskA()
      replacementTask.queueState.localComplete = true

      spyOn(TaskQueue, 'dequeue').andCallThrough()
      TaskQueue._queue = [obsoleteTask, otherTask]
      TaskQueue._dequeueObsoleteTasks(replacementTask)
      expect(TaskQueue._queue.length).toBe(1)
      expect(TaskQueue.dequeue).toHaveBeenCalledWith(obsoleteTask)
      expect(TaskQueue.dequeue.calls.length).toBe(1)

  describe "dequeue", ->
    beforeEach ->
      TaskQueue._queue = [@unstartedTask, @processingTask]

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

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

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

    describe "with an unstarted task", ->
      it "moves it from the queue", ->
        TaskQueue.dequeue(@unstartedTask)
        expect(TaskQueue._queue.length).toBe(1)
        expect(TaskQueue._completed.length).toBe(1)

      it "notifies the queue has been updated", ->
        spyOn(TaskQueue, "_processQueue")
        TaskQueue.dequeue(@unstartedTask)
        advanceClock(20)
        expect(TaskQueue._processQueue).toHaveBeenCalled()
        expect(TaskQueue._processQueue.calls.length).toBe(1)

    describe "with a processing task", ->
      it "calls cancel() to allow the task to resolve or reject from runRemote()", ->
        spyOn(@processingTask, 'cancel')
        TaskQueue.dequeue(@processingTask)
        expect(@processingTask.cancel).toHaveBeenCalled()
        expect(TaskQueue._queue.length).toBe(2)
        expect(TaskQueue._completed.length).toBe(0)

  describe "process Task", ->
    it "doesn't process processing tasks", ->
      spyOn(@processingTask, "runRemote").andCallFake -> Promise.resolve()
      TaskQueue._processTask(@processingTask)
      expect(@processingTask.runRemote).not.toHaveBeenCalled()

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

      taskA = new TaskSubclassA()
      otherTask = new Task()
      blockedByTaskA = new BlockedByTaskA()

      taskA.queueState.localComplete = true
      otherTask.queueState.localComplete = true
      blockedByTaskA.queueState.localComplete = true

      spyOn(taskA, "runRemote").andCallFake -> new Promise (resolve, reject) ->
      spyOn(blockedByTaskA, "runRemote").andCallFake -> Promise.resolve()

      TaskQueue._queue = [taskA, otherTask, blockedByTaskA]
      TaskQueue._processQueue()

      advanceClock()

      expect(TaskQueue._queue.length).toBe(2)
      expect(taskA.runRemote).toHaveBeenCalled()
      expect(blockedByTaskA.runRemote).not.toHaveBeenCalled()

    it "doesn't block itself, even if the shouldWaitForTask method is implemented naively", ->
      class BlockingTask extends Task
        shouldWaitForTask: (other) -> other instanceof BlockingTask

      blockedTask = new BlockingTask()
      spyOn(blockedTask, "runRemote").andCallFake -> Promise.resolve()

      TaskQueue.enqueue(blockedTask)
      advanceClock()
      blockedTask.runRemote.callCount > 0

    it "sets the processing bit", ->
      spyOn(@unstartedTask, "runRemote").andCallFake -> Promise.resolve()
      task = new Task()
      task.queueState.localComplete = true
      TaskQueue._queue = [task]
      TaskQueue._processTask(task)
      expect(task.queueState.isProcessing).toBe true

