_ = require 'underscore'
Label = require '../../src/flux/models/label'
Thread = require '../../src/flux/models/thread'
Message = require '../../src/flux/models/message'
Actions = require '../../src/flux/actions'
NylasAPI = require '../../src/flux/nylas-api'
DatabaseStore = require '../../src/flux/stores/database-store'
ChangeLabelsTask = require '../../src/flux/tasks/change-labels-task'

{APIError} = require '../../src/flux/errors'
{Utils} = require '../../src/flux/models/utils'

testLabels = {}
testThreads = {}
testMessages = {}

describe "ChangeLabelsTask", ->
  beforeEach ->
    spyOn(DatabaseStore, 'persistModel').andCallFake -> Promise.resolve()
    spyOn(DatabaseStore, 'persistModels').andCallFake -> Promise.resolve()
    spyOn(DatabaseStore, 'find').andCallFake (klass, id) =>
      if klass is Thread
        Promise.resolve(testThreads[id])
      else if klass is Message
        Promise.resolve(testMessages[id])
      else if klass is Label
        Promise.resolve(testLabels[id])
      else
        throw new Error("Not stubbed!")

    spyOn(DatabaseStore, 'findAll').andCallFake (klass, finder) =>
      if klass is Message
        Promise.resolve(_.values(testMessages))
      else if klass is Thread
        Promise.resolve(_.values(testThreads))
      else if klass is Label
        Promise.resolve(_.values(testLabels))
      else
        throw new Error("Not stubbed!")

    testLabels = @testLabels =
      "l1": new Label({name: 'inbox', id: 'l1', displayName: "INBOX"}),
      "l2": new Label({name: 'drafts', id: 'l2', displayName: "MyDrafts"})
      "l3": new Label({name: null, id: 'l3', displayName: "My Label"})

    testThreads = @testThreads =
      't1': new Thread(id: 't1', labels: [@testLabels['l1']])
      't2': new Thread(id: 't2', labels: _.values(@testLabels))
      't3': new Thread(id: 't3', labels: [@testLabels['l2'], @testLabels['l3']])

    testMessages = @testMessages =
      'm1': new Message(id: 'm1', labels: [@testLabels['l1']])
      'm2': new Message(id: 'm2', labels: _.values(@testLabels))
      'm3': new Message(id: 'm3', labels: [@testLabels['l2'], @testLabels['l3']])

    @basicThreadTask = new ChangeLabelsTask
      labelsToAdd: ["l1", "l2"]
      labelsToRemove: ["l3"]
      threadIds: ['t1']

    @basicMessageTask = new ChangeLabelsTask
      labelsToAdd: ["l1", "l2"]
      labelsToRemove: ["l3"]
      messageIds: ['m1']

  describe "description", ->
    it "should include the name of the added label if it's the only mutation and it was provided as an object", ->
      task = new ChangeLabelsTask(labelsToAdd: ["l1"], labelsToRemove: [], threadIds: ['t1'])
      expect(task.description()).toEqual("Changed labels on 1 thread")
      task = new ChangeLabelsTask(labelsToAdd: [new Label(id: 'l1', displayName: 'LABEL')], labelsToRemove: [], threadIds: ['t1'])
      expect(task.description()).toEqual("Added LABEL to 1 thread")
      task = new ChangeLabelsTask(labelsToAdd: [new Label(id: 'l1', displayName: 'LABEL')], labelsToRemove: ['l2'], threadIds: ['t1'])
      expect(task.description()).toEqual("Changed labels on 1 thread")

    it "should include the name of the removed label if it's the only mutation and it was provided as an object", ->
      task = new ChangeLabelsTask(labelsToAdd: [], labelsToRemove: ["l1"], threadIds: ['t1'])
      expect(task.description()).toEqual("Changed labels on 1 thread")
      task = new ChangeLabelsTask(labelsToAdd: [], labelsToRemove: [new Label(id: 'l1', displayName: 'LABEL')], threadIds: ['t1'])
      expect(task.description()).toEqual("Removed LABEL from 1 thread")
      task = new ChangeLabelsTask(labelsToAdd: ['l2'], labelsToRemove: [new Label(id: 'l1', displayName: 'LABEL')], threadIds: ['t1'])
      expect(task.description()).toEqual("Changed labels on 1 thread")

    it "should pluralize properly", ->
      task = new ChangeLabelsTask(labelsToAdd: ["l2"], labelsToRemove: ["l1"], threadIds: ['t1', 't2', 't3'])
      expect(task.description()).toEqual("Changed labels on 3 threads")

  describe "shouldWaitForTask", ->
    it "should return true if another, older ChangeLabelsTask involves the same threads", ->
      a = new ChangeLabelsTask(threadIds: ['t1', 't2', 't3'])
      a.creationDate = new Date(1000)
      b = new ChangeLabelsTask(threadIds: ['t3', 't4', 't7'])
      b.creationDate = new Date(2000)
      c = new ChangeLabelsTask(threadIds: ['t0', 't7'])
      c.creationDate = new Date(3000)
      expect(a.shouldWaitForTask(b)).toEqual(false)
      expect(a.shouldWaitForTask(c)).toEqual(false)
      expect(b.shouldWaitForTask(a)).toEqual(true)
      expect(c.shouldWaitForTask(a)).toEqual(false)
      expect(c.shouldWaitForTask(b)).toEqual(true)

  describe "performLocal", ->
    it "should throw an exception if task has not been given a thread", ->
      badTasks = [
        new ChangeLabelsTask(),
        new ChangeLabelsTask(threadIds: [123]),
        new ChangeLabelsTask(threadIds: [123], messageIds: ["foo"]),
        new ChangeLabelsTask(threadIds: "Thread"),
      ]
      goodTasks = [
        new ChangeLabelsTask(
          labelsToAdd: ['l2']
          labelsToRemove: ['l1']
          threadIds: ['t1']
        )
        new ChangeLabelsTask(
          labelsToAdd: ['l2']
          labelsToRemove: []
          messageIds: ['m1']
        )
      ]
      caught = []
      succeeded = []

      runs ->
        [].concat(badTasks, goodTasks).forEach (task) ->
          task.performLocal()
          .then -> succeeded.push(task)
          .catch (err) -> caught.push(task)
      waitsFor ->
        succeeded.length + caught.length == 6
      runs ->
        expect(caught.length).toEqual(badTasks.length)
        expect(succeeded.length).toEqual(goodTasks.length)

    it 'finds all of the labels to add by id', ->
      waitsForPromise =>
        @basicThreadTask.collectCategories().then (categories) =>
          expect(categories.labelsToAdd).toEqual [@testLabels['l1'], @testLabels['l2']]
          expect(categories.labelsToRemove).toEqual [@testLabels['l3']]

    it 'finds all of the labels to add by object', ->
      task = new ChangeLabelsTask
        labelsToAdd: [@testLabels['l1'], @testLabels['l2']]
        labelsToRemove: []
        threadIds: ['t1']

      waitsForPromise =>
        task.collectCategories().then (categories) =>
          expect(categories.labelsToAdd).toEqual [@testLabels['l1'], @testLabels['l2']]
          expect(categories.labelsToRemove).toEqual []

    it 'increments optimistic changes', ->
      spyOn(@basicThreadTask, "localUpdateThread").andReturn Promise.resolve()
      spyOn(NylasAPI, "incrementOptimisticChangeCount")
      @basicThreadTask.performLocal().then ->
        expect(NylasAPI.incrementOptimisticChangeCount)
          .toHaveBeenCalledWith(Thread, 't1')

    it 'decrements optimistic changes if reverting', ->
      spyOn(@basicThreadTask, "localUpdateThread").andReturn Promise.resolve()
      spyOn(NylasAPI, "decrementOptimisticChangeCount")
      @basicThreadTask.performLocal(reverting: true).then ->
        expect(NylasAPI.decrementOptimisticChangeCount)
          .toHaveBeenCalledWith(Thread, 't1')

    describe 'when creating a _newLabelSet', ->
      it 'properly adds labels', ->
        t1 = @testThreads['t1']
        toAdd = [@testLabels['l1'], @testLabels['l2']]
        out = @basicThreadTask._newLabelSet(t1, labelsToAdd: toAdd)
        expect(out).toEqual toAdd

      it 'properly removes labels', ->
        t3 = @testThreads['t3']
        toRemove = [@testLabels['l1'], @testLabels['l2']]
        out = @basicThreadTask._newLabelSet(t3, labelsToRemove: toRemove)
        expect(out).toEqual [@testLabels['l3']]

      it 'properly adds and removes labels', ->
        t1 = @testThreads['t1']
        toAdd = [@testLabels['l1'], @testLabels['l2']]
        toRemove = [@testLabels['l2'], @testLabels['l3']]
        out = @basicThreadTask._newLabelSet(t1, labelsToAdd: toAdd, labelsToRemove: toRemove)
        expect(out).toEqual [@testLabels['l1']]

    it 'updates a thread with the new labels', ->
      expectedLabels = [@testLabels['l1'], @testLabels['l2']]
      @basicThreadTask.performLocal().then ->
        thread = DatabaseStore.persistModel.calls[0].args[0]
        expect(thread.labels).toEqual expectedLabels

    it "updates a thread's messages with the new labels", ->
      # Our stub of DatabaseStore.findAll ignores the scoping parameter.
      # We simply return all messages.

      expectedLabels = [@testLabels['l1'], @testLabels['l2']]
      @basicThreadTask.performLocal().then ->
        messages = DatabaseStore.persistModels.calls[0].args[0]
        expect(messages.length).toBe 3
        for message in messages
          expect(message.labels).toEqual expectedLabels

    it "doesn't botter updating the message if it already has the correct labels", ->
      @testMessages['m4'] =
        new Message(id: 'm4', labels: [@testLabels['l1'], @testLabels['l2']])
      @testMessages['m5'] =
        new Message(id: 'm5', labels: [])

      expectedLabels = [@testLabels['l1'], @testLabels['l2']]
      @basicThreadTask.performLocal().then =>
        messages = DatabaseStore.persistModels.calls[0].args[0]
        expect(messages.length).toBe 4
        for message in messages
          expect(message.labels).toEqual expectedLabels
        expect(@testMessages['m4'] not in messages).toBe true

    it 'updates a message with the new labels on a message task', ->
      expectedLabels = [@testLabels['l1'], @testLabels['l2']]
      @basicMessageTask.performLocal().then ->
        thread = DatabaseStore.persistModel.calls[0].args[0]
        expect(thread.labels).toEqual expectedLabels

    it 'saves the new label set to an instance variable on the task so performRemote can access it later', ->
      expectedLabels = [@testLabels['l1'], @testLabels['l2']]
      @basicThreadTask.performLocal().then =>
        expect(@basicThreadTask._newLabels['t1']).toEqual expectedLabels

  describe 'performRemote', ->
    beforeEach ->
      spyOn(NylasAPI, "makeRequest").andCallFake (options) ->
        options.beforeProcessing?(options.body)
        return Promise.resolve()

      @multiThreadTask = new ChangeLabelsTask
        labelsToAdd: ["l1", "l2"]
        labelsToRemove: ["l3"]
        threadIds: ['t1', 't2']

      @multiMessageTask = new ChangeLabelsTask
        labelsToAdd: ["l1", "l2"]
        labelsToRemove: ["l3"]
        messageIds: ['m1', 'm2']

      expectedLabels = [@testLabels['l1'], @testLabels['l2']]
      @multiThreadTask._newLabels['t1'] = expectedLabels
      @multiThreadTask._newLabels['t2'] = expectedLabels
      @multiMessageTask._newLabels['m1'] = expectedLabels
      @multiMessageTask._newLabels['m2'] = expectedLabels

    it 'makes a new request object for each object', ->
      @multiThreadTask.performRemote().then ->
        expect(NylasAPI.makeRequest.calls.length).toBe 2

    it 'decrements the optimistic change count on each request', ->
      spyOn(NylasAPI, "decrementOptimisticChangeCount")
      @multiThreadTask.performRemote().then ->
        klass = NylasAPI.decrementOptimisticChangeCount.calls[0].args[0]
        expect(NylasAPI.decrementOptimisticChangeCount.calls.length).toBe 2
        expect(klass).toBe Thread

    it 'decrements the optimistic change for messages too', ->
      spyOn(NylasAPI, "decrementOptimisticChangeCount")
      @multiMessageTask.performRemote().then ->
        klass = NylasAPI.decrementOptimisticChangeCount.calls[0].args[0]
        expect(NylasAPI.decrementOptimisticChangeCount.calls.length).toBe 2
        expect(klass).toBe Message

    it 'properly passes the label IDs to the body', ->
      @multiThreadTask.performRemote().then ->
        opts = NylasAPI.makeRequest.calls[0].args[0]
        expect(opts.body).toEqual labels: ['l1', 'l2']

    it 'gets the correct endpoint for the thread tasks', ->
      @multiThreadTask.performRemote().then ->
        opts = NylasAPI.makeRequest.calls[0].args[0]
        expect(opts.path).toEqual "/n/nsid/threads/t1"

    it 'gets the correct endpoint for the message tasks', ->
      @multiMessageTask.performRemote().then ->
        opts = NylasAPI.makeRequest.calls[0].args[0]
        expect(opts.path).toEqual "/n/nsid/messages/m1"
