const {it, fit, ffit, beforeEach, afterEach, conditionPromise} = require('./async-spec-helpers')
const _ = require('underscore-plus')
const fs = require('fs')
const path = require('path')
const temp = require('temp').track()
const AtomEnvironment = require('../src/atom-environment')

describe('AtomEnvironment', () => {
  afterEach(() => {
    try {
      temp.cleanupSync()
    } catch (error) {}
  })

  describe('window sizing methods', () => {
    describe('::getPosition and ::setPosition', () => {
      let originalPosition = null
      beforeEach(() => originalPosition = atom.getPosition())

      afterEach(() => atom.setPosition(originalPosition.x, originalPosition.y))

      it('sets the position of the window, and can retrieve the position just set', () => {
        atom.setPosition(22, 45)
        expect(atom.getPosition()).toEqual({x: 22, y: 45})
      })
    })

    describe('::getSize and ::setSize', () => {
      let originalSize = null
      beforeEach(() => originalSize = atom.getSize())
      afterEach(() => atom.setSize(originalSize.width, originalSize.height))

      it('sets the size of the window, and can retrieve the size just set', async () => {
        const newWidth = originalSize.width - 12
        const newHeight = originalSize.height - 23
        await atom.setSize(newWidth, newHeight)
        expect(atom.getSize()).toEqual({width: newWidth, height: newHeight})
      })
    })
  })

  describe('.isReleasedVersion()', () => {
    it('returns false if the version is a SHA and true otherwise', () => {
      let version = '0.1.0'
      spyOn(atom, 'getVersion').andCallFake(() => version)
      expect(atom.isReleasedVersion()).toBe(true)
      version = '36b5518'
      expect(atom.isReleasedVersion()).toBe(false)
    })
  })

  describe('loading default config', () => {
    it('loads the default core config schema', () => {
      expect(atom.config.get('core.excludeVcsIgnoredPaths')).toBe(true)
      expect(atom.config.get('core.followSymlinks')).toBe(true)
      expect(atom.config.get('editor.showInvisibles')).toBe(false)
    })
  })

  describe('window onerror handler', () => {
    let devToolsPromise = null
    beforeEach(() => {
      devToolsPromise = Promise.resolve()
      spyOn(atom, 'openDevTools').andReturn(devToolsPromise)
      spyOn(atom, 'executeJavaScriptInDevTools')
    })

    it('will open the dev tools when an error is triggered', async () => {
      try {
        a + 1
      } catch (e) {
        window.onerror.call(window, e.toString(), 'abc', 2, 3, e)
      }

      await devToolsPromise
      expect(atom.openDevTools).toHaveBeenCalled()
      expect(atom.executeJavaScriptInDevTools).toHaveBeenCalled()
    })

    describe('::onWillThrowError', () => {
      let willThrowSpy = null

      beforeEach(() => {
        willThrowSpy = jasmine.createSpy()
      })

      it('is called when there is an error', () => {
        let error = null
        atom.onWillThrowError(willThrowSpy)
        try {
          a + 1
        } catch (e) {
          error = e
          window.onerror.call(window, e.toString(), 'abc', 2, 3, e)
        }

        delete willThrowSpy.mostRecentCall.args[0].preventDefault
        expect(willThrowSpy).toHaveBeenCalledWith({
          message: error.toString(),
          url: 'abc',
          line: 2,
          column: 3,
          originalError: error
        })
      })

      it('will not show the devtools when preventDefault() is called', () => {
        willThrowSpy.andCallFake(errorObject => errorObject.preventDefault())
        atom.onWillThrowError(willThrowSpy)

        try {
          a + 1
        } catch (e) {
          window.onerror.call(window, e.toString(), 'abc', 2, 3, e)
        }

        expect(willThrowSpy).toHaveBeenCalled()
        expect(atom.openDevTools).not.toHaveBeenCalled()
        expect(atom.executeJavaScriptInDevTools).not.toHaveBeenCalled()
      })
    })

    describe('::onDidThrowError', () => {
      let didThrowSpy = null
      beforeEach(() => didThrowSpy = jasmine.createSpy())

      it('is called when there is an error', () => {
        let error = null
        atom.onDidThrowError(didThrowSpy)
        try {
          a + 1
        } catch (e) {
          error = e
          window.onerror.call(window, e.toString(), 'abc', 2, 3, e)
        }
        expect(didThrowSpy).toHaveBeenCalledWith({
          message: error.toString(),
          url: 'abc',
          line: 2,
          column: 3,
          originalError: error
        })
      })
    })
  })

  describe('.assert(condition, message, callback)', () => {
    let errors = null

    beforeEach(() => {
      errors = []
      spyOn(atom, 'isReleasedVersion').andReturn(true)
      atom.onDidFailAssertion(error => errors.push(error))
    })

    describe('if the condition is false', () => {
      it('notifies onDidFailAssertion handlers with an error object based on the call site of the assertion', () => {
        const result = atom.assert(false, 'a == b')
        expect(result).toBe(false)
        expect(errors.length).toBe(1)
        expect(errors[0].message).toBe('Assertion failed: a == b')
        expect(errors[0].stack).toContain('atom-environment-spec')
      })

      describe('if passed a callback function', () => {
        it("calls the callback with the assertion failure's error object", () => {
          let error = null
          atom.assert(false, 'a == b', e => error = e)
          expect(error).toBe(errors[0])
        })
      })

      describe('if passed metadata', () => {
        it("assigns the metadata on the assertion failure's error object", () => {
          atom.assert(false, 'a == b', {foo: 'bar'})
          expect(errors[0].metadata).toEqual({foo: 'bar'})
        })
      })

      describe('when Atom has been built from source', () => {
        it('throws an error', () => {
          atom.isReleasedVersion.andReturn(false)
          expect(() => atom.assert(false, 'testing')).toThrow('Assertion failed: testing')
        })
      })
    })

    describe('if the condition is true', () => {
      it('does nothing', () => {
        const result = atom.assert(true, 'a == b')
        expect(result).toBe(true)
        expect(errors).toEqual([])
      })
    })
  })

  describe('saving and loading', () => {
    beforeEach(() => atom.enablePersistence = true)

    afterEach(() => atom.enablePersistence = false)

    it('selects the state based on the current project paths', async () => {
      jasmine.useRealClock()

      const [dir1, dir2] = [temp.mkdirSync('dir1-'), temp.mkdirSync('dir2-')]

      const loadSettings = Object.assign(atom.getLoadSettings(), {
        initialPaths: [dir1],
        windowState: null
      })

      spyOn(atom, 'getLoadSettings').andCallFake(() => loadSettings)
      spyOn(atom, 'serialize').andReturn({stuff: 'cool'})

      atom.project.setPaths([dir1, dir2])

      // State persistence will fail if other Atom instances are running
      expect(await atom.stateStore.connect()).toBe(true)

      await atom.saveState()
      expect(await atom.loadState()).toBeFalsy()

      loadSettings.initialPaths = [dir2, dir1]
      expect(await atom.loadState()).toEqual({stuff: 'cool'})
    })

    it('saves state when the CPU is idle after a keydown or mousedown event', () => {
      const atomEnv = new AtomEnvironment({
        applicationDelegate: global.atom.applicationDelegate
      })
      const idleCallbacks = []
      atomEnv.initialize({
        window: {
          requestIdleCallback (callback) { idleCallbacks.push(callback) },
          addEventListener () {},
          removeEventListener () {}
        },
        document: document.implementation.createHTMLDocument()
      })

      spyOn(atomEnv, 'saveState')

      const keydown = new KeyboardEvent('keydown')
      atomEnv.document.dispatchEvent(keydown)
      advanceClock(atomEnv.saveStateDebounceInterval)
      idleCallbacks.shift()()
      expect(atomEnv.saveState).toHaveBeenCalledWith({isUnloading: false})
      expect(atomEnv.saveState).not.toHaveBeenCalledWith({isUnloading: true})

      atomEnv.saveState.reset()
      const mousedown = new MouseEvent('mousedown')
      atomEnv.document.dispatchEvent(mousedown)
      advanceClock(atomEnv.saveStateDebounceInterval)
      idleCallbacks.shift()()
      expect(atomEnv.saveState).toHaveBeenCalledWith({isUnloading: false})
      expect(atomEnv.saveState).not.toHaveBeenCalledWith({isUnloading: true})

      atomEnv.destroy()
    })

    it('ignores mousedown/keydown events happening after calling unloadEditorWindow', () => {
      const atomEnv = new AtomEnvironment({
        applicationDelegate: global.atom.applicationDelegate
      })
      const idleCallbacks = []
      atomEnv.initialize({
        window: {
          requestIdleCallback (callback) { idleCallbacks.push(callback) },
          addEventListener () {},
          removeEventListener () {}
        },
        document: document.implementation.createHTMLDocument()
      })

      spyOn(atomEnv, 'saveState')

      let mousedown = new MouseEvent('mousedown')
      atomEnv.document.dispatchEvent(mousedown)
      atomEnv.unloadEditorWindow()
      expect(atomEnv.saveState).not.toHaveBeenCalled()

      advanceClock(atomEnv.saveStateDebounceInterval)
      idleCallbacks.shift()()
      expect(atomEnv.saveState).not.toHaveBeenCalled()

      mousedown = new MouseEvent('mousedown')
      atomEnv.document.dispatchEvent(mousedown)
      advanceClock(atomEnv.saveStateDebounceInterval)
      idleCallbacks.shift()()
      expect(atomEnv.saveState).not.toHaveBeenCalled()

      atomEnv.destroy()
    })

    it('serializes the project state with all the options supplied in saveState', async () => {
      spyOn(atom.project, 'serialize').andReturn({foo: 42})

      await atom.saveState({anyOption: 'any option'})
      expect(atom.project.serialize.calls.length).toBe(1)
      expect(atom.project.serialize.mostRecentCall.args[0]).toEqual({anyOption: 'any option'})
    })

    it('serializes the text editor registry', async () => {
      await atom.packages.activatePackage('language-text')
      const editor = await atom.workspace.open('sample.js')
      expect(atom.grammars.assignLanguageMode(editor, 'text.plain')).toBe(true)

      const atom2 = new AtomEnvironment({
        applicationDelegate: atom.applicationDelegate,
        window: document.createElement('div'),
        document: Object.assign(
          document.createElement('div'),
          {
            body: document.createElement('div'),
            head: document.createElement('div')
          }
        )
      })
      atom2.initialize({document, window})

      await atom2.deserialize(atom.serialize())
      await atom2.packages.activatePackage('language-text')
      const editor2 = atom2.workspace.getActiveTextEditor()
      expect(editor2.getBuffer().getLanguageMode().getLanguageId()).toBe('text.plain')
      atom2.destroy()
    })

    describe('deserialization failures', () => {
      it('propagates project state restoration failures', async () => {
        spyOn(atom.project, 'deserialize').andCallFake(() => {
          const err = new Error('deserialization failure')
          err.missingProjectPaths = ['/foo']
          return Promise.reject(err)
        })
        spyOn(atom.notifications, 'addError')

        await atom.deserialize({project: 'should work'})
        expect(atom.notifications.addError).toHaveBeenCalledWith('Unable to open project directory', {
          description: 'Project directory `/foo` is no longer on disk.'
        })
      })

      it('accumulates and reports two errors with one notification', async () => {
        spyOn(atom.project, 'deserialize').andCallFake(() => {
          const err = new Error('deserialization failure')
          err.missingProjectPaths = ['/foo', '/wat']
          return Promise.reject(err)
        })
        spyOn(atom.notifications, 'addError')

        await atom.deserialize({project: 'should work'})
        expect(atom.notifications.addError).toHaveBeenCalledWith('Unable to open 2 project directories', {
          description: 'Project directories `/foo` and `/wat` are no longer on disk.'
        })
      })

      it('accumulates and reports three+ errors with one notification', async () => {
        spyOn(atom.project, 'deserialize').andCallFake(() => {
          const err = new Error('deserialization failure')
          err.missingProjectPaths = ['/foo', '/wat', '/stuff', '/things']
          return Promise.reject(err)
        })
        spyOn(atom.notifications, 'addError')

        await atom.deserialize({project: 'should work'})
        expect(atom.notifications.addError).toHaveBeenCalledWith('Unable to open 4 project directories', {
          description: 'Project directories `/foo`, `/wat`, `/stuff`, and `/things` are no longer on disk.'
        })
      })
    })
  })

  describe('openInitialEmptyEditorIfNecessary', () => {
    describe('when there are no paths set', () => {
      beforeEach(() => spyOn(atom, 'getLoadSettings').andReturn({initialPaths: []}))

      it('opens an empty buffer', () => {
        spyOn(atom.workspace, 'open')
        atom.openInitialEmptyEditorIfNecessary()
        expect(atom.workspace.open).toHaveBeenCalledWith(null)
      })

      describe('when there is already a buffer open', () => {
        beforeEach(async () => {
          await atom.workspace.open()
        })

        it('does not open an empty buffer', () => {
          spyOn(atom.workspace, 'open')
          atom.openInitialEmptyEditorIfNecessary()
          expect(atom.workspace.open).not.toHaveBeenCalled()
        })
      })
    })

    describe('when the project has a path', () => {
      beforeEach(() => {
        spyOn(atom, 'getLoadSettings').andReturn({initialPaths: ['something']})
        spyOn(atom.workspace, 'open')
      })

      it('does not open an empty buffer', () => {
        atom.openInitialEmptyEditorIfNecessary()
        expect(atom.workspace.open).not.toHaveBeenCalled()
      })
    })
  })

  describe('adding a project folder', () => {
    it('does nothing if the user dismisses the file picker', () => {
      const initialPaths = atom.project.getPaths()
      const tempDirectory = temp.mkdirSync('a-new-directory')
      spyOn(atom, 'pickFolder').andCallFake(callback => callback(null))
      atom.addProjectFolder()
      expect(atom.project.getPaths()).toEqual(initialPaths)
    })

    describe('when there is no saved state for the added folders', () => {
      beforeEach(() => {
        spyOn(atom, 'loadState').andReturn(Promise.resolve(null))
        spyOn(atom, 'attemptRestoreProjectStateForPaths')
      })

      it('adds the selected folder to the project', async () => {
        const initialPaths = atom.project.setPaths([])
        const tempDirectory = temp.mkdirSync('a-new-directory')
        spyOn(atom, 'pickFolder').andCallFake(callback => callback([tempDirectory]))
        await atom.addProjectFolder()
        expect(atom.project.getPaths()).toEqual([tempDirectory])
        expect(atom.attemptRestoreProjectStateForPaths).not.toHaveBeenCalled()
      })
    })

    describe('when there is saved state for the relevant directories', () => {
      const state = Symbol('savedState')

      beforeEach(() => {
        spyOn(atom, 'getStateKey').andCallFake(dirs => dirs.join(':'))
        spyOn(atom, 'loadState').andCallFake(async (key) => key === __dirname ? state : null)
        spyOn(atom, 'attemptRestoreProjectStateForPaths')
        spyOn(atom, 'pickFolder').andCallFake(callback => callback([__dirname]))
        atom.project.setPaths([])
      })

      describe('when there are no project folders', () => {
        it('attempts to restore the project state', async () => {
          await atom.addProjectFolder()
          expect(atom.attemptRestoreProjectStateForPaths).toHaveBeenCalledWith(state, [__dirname])
          expect(atom.project.getPaths()).toEqual([])
        })
      })

      describe('when there are already project folders', () => {
        const openedPath = path.join(__dirname, 'fixtures')

        beforeEach(() => atom.project.setPaths([openedPath]))

        it('does not attempt to restore the project state, instead adding the project paths', async () => {
          await atom.addProjectFolder()
          expect(atom.attemptRestoreProjectStateForPaths).not.toHaveBeenCalled()
          expect(atom.project.getPaths()).toEqual([openedPath, __dirname])
        })
      })
    })
  })

  describe('attemptRestoreProjectStateForPaths(state, projectPaths, filesToOpen)', () => {
    describe('when the window is clean (empty or has only unnamed, unmodified buffers)', () => {
      beforeEach(async () => {
        // Unnamed, unmodified buffer doesn't count toward "clean"-ness
        await atom.workspace.open()
      })

      it('automatically restores the saved state into the current environment', async () => {
        const projectPath = temp.mkdirSync()
        const filePath1 = path.join(projectPath, 'file-1')
        const filePath2 = path.join(projectPath, 'file-2')
        const filePath3 = path.join(projectPath, 'file-3')
        fs.writeFileSync(filePath1, 'abc')
        fs.writeFileSync(filePath2, 'def')
        fs.writeFileSync(filePath3, 'ghi')

        const env1 = new AtomEnvironment({applicationDelegate: atom.applicationDelegate})
        env1.project.setPaths([projectPath])
        await env1.workspace.open(filePath1)
        await env1.workspace.open(filePath2)
        await env1.workspace.open(filePath3)
        const env1State = env1.serialize()
        env1.destroy()

        const env2 = new AtomEnvironment({applicationDelegate: atom.applicationDelegate})
        await env2.attemptRestoreProjectStateForPaths(env1State, [projectPath], [filePath2])
        const restoredURIs = env2.workspace.getPaneItems().map(p => p.getURI())
        expect(restoredURIs).toEqual([filePath1, filePath2, filePath3])
        env2.destroy()
      })

      describe('when a dock has a non-text editor', () => {
        it("doesn't prompt the user to restore state", () => {
          const dock = atom.workspace.getLeftDock()
          dock.getActivePane().addItem({
            getTitle () { return 'title' },
            element: document.createElement('div')
          })
          const state = {}
          spyOn(atom, 'confirm')
          atom.attemptRestoreProjectStateForPaths(state, [__dirname], [__filename])
          expect(atom.confirm).not.toHaveBeenCalled()
        })
      })
    })

    describe('when the window is dirty', () => {
      let editor

      beforeEach(async () => {
        editor = await atom.workspace.open()
        editor.setText('new editor')
      })

      describe('when a dock has a modified editor', () => {
        it('prompts the user to restore the state', () => {
          const dock = atom.workspace.getLeftDock()
          dock.getActivePane().addItem(editor)
          spyOn(atom, 'confirm').andReturn(1)
          spyOn(atom.project, 'addPath')
          spyOn(atom.workspace, 'open')
          const state = Symbol()
          atom.attemptRestoreProjectStateForPaths(state, [__dirname], [__filename])
          expect(atom.confirm).toHaveBeenCalled()
        })
      })

      it('prompts the user to restore the state in a new window, discarding it and adding folder to current window', async () => {
        jasmine.useRealClock()
        spyOn(atom, 'confirm').andCallFake((options, callback) => callback(1))
        spyOn(atom.project, 'addPath')
        spyOn(atom.workspace, 'open')
        const state = Symbol()

        atom.attemptRestoreProjectStateForPaths(state, [__dirname], [__filename])
        expect(atom.confirm).toHaveBeenCalled()
        await conditionPromise(() => atom.project.addPath.callCount === 1)

        expect(atom.project.addPath).toHaveBeenCalledWith(__dirname)
        expect(atom.workspace.open.callCount).toBe(1)
        expect(atom.workspace.open).toHaveBeenCalledWith(__filename)
      })

      it('prompts the user to restore the state in a new window, opening a new window', async () => {
        jasmine.useRealClock()
        spyOn(atom, 'confirm').andCallFake((options, callback) => callback(0))
        spyOn(atom, 'open')
        const state = Symbol()

        atom.attemptRestoreProjectStateForPaths(state, [__dirname], [__filename])
        expect(atom.confirm).toHaveBeenCalled()
        await conditionPromise(() => atom.open.callCount === 1)
        expect(atom.open).toHaveBeenCalledWith({
          pathsToOpen: [__dirname, __filename],
          newWindow: true,
          devMode: atom.inDevMode(),
          safeMode: atom.inSafeMode()
        })
      })
    })
  })

  describe('::unloadEditorWindow()', () => {
    it('saves the BlobStore so it can be loaded after reload', () => {
      const configDirPath = temp.mkdirSync('atom-spec-environment')
      const fakeBlobStore = jasmine.createSpyObj('blob store', ['save'])
      const atomEnvironment = new AtomEnvironment({applicationDelegate: atom.applicationDelegate, enablePersistence: true})
      atomEnvironment.initialize({configDirPath, blobStore: fakeBlobStore, window, document})

      atomEnvironment.unloadEditorWindow()

      expect(fakeBlobStore.save).toHaveBeenCalled()

      atomEnvironment.destroy()
    })
  })

  describe('::destroy()', () => {
    it('does not throw exceptions when unsubscribing from ipc events (regression)', async () => {
      const configDirPath = temp.mkdirSync('atom-spec-environment')
      const fakeDocument = {
        addEventListener () {},
        removeEventListener () {},
        head: document.createElement('head'),
        body: document.createElement('body')
      }
      const atomEnvironment = new AtomEnvironment({applicationDelegate: atom.applicationDelegate})
      atomEnvironment.initialize({window, document: fakeDocument})
      spyOn(atomEnvironment.packages, 'loadPackages').andReturn(Promise.resolve())
      spyOn(atomEnvironment.packages, 'activate').andReturn(Promise.resolve())
      spyOn(atomEnvironment, 'displayWindow').andReturn(Promise.resolve())
      await atomEnvironment.startEditorWindow()
      atomEnvironment.unloadEditorWindow()
      atomEnvironment.destroy()
    })
  })

  describe('::whenShellEnvironmentLoaded()', () => {
    let atomEnvironment, envLoaded, spy

    beforeEach(() => {
      let resolve = null
      const promise = new Promise((r) => { resolve = r })
      envLoaded = () => {
        resolve()
        return promise
      }
      atomEnvironment = new AtomEnvironment({
        applicationDelegate: atom.applicationDelegate,
        updateProcessEnv () { return promise }
      })
      atomEnvironment.initialize({window, document})
      spy = jasmine.createSpy()
    })

    afterEach(() => atomEnvironment.destroy())

    it('is triggered once the shell environment is loaded', async () => {
      atomEnvironment.whenShellEnvironmentLoaded(spy)
      atomEnvironment.updateProcessEnvAndTriggerHooks()
      await envLoaded()
      expect(spy).toHaveBeenCalled()
    })

    it('triggers the callback immediately if the shell environment is already loaded', async () => {
      atomEnvironment.updateProcessEnvAndTriggerHooks()
      await envLoaded()
      atomEnvironment.whenShellEnvironmentLoaded(spy)
      expect(spy).toHaveBeenCalled()
    })
  })

  describe('::openLocations(locations) (called via IPC from browser process)', () => {
    beforeEach(() => {
      spyOn(atom.workspace, 'open')
      atom.project.setPaths([])
    })

    describe('when there is no saved state', () => {
      beforeEach(() => {
        spyOn(atom, 'loadState').andReturn(Promise.resolve(null))
      })

      describe('when the opened path exists', () => {
        it("adds it to the project's paths", async () => {
          const pathToOpen = __filename
          await atom.openLocations([{pathToOpen}])
          expect(atom.project.getPaths()[0]).toBe(__dirname)
        })

        describe('then a second path is opened with forceAddToWindow', () => {
          it("adds the second path to the project's paths", async () => {
            const firstPathToOpen = __dirname
            const secondPathToOpen = path.resolve(__dirname, './fixtures')
            await atom.openLocations([{pathToOpen: firstPathToOpen}])
            await atom.openLocations([{pathToOpen: secondPathToOpen, forceAddToWindow: true}])
            expect(atom.project.getPaths()).toEqual([firstPathToOpen, secondPathToOpen])
          })
        })
      })

      describe('when the opened path does not exist but its parent directory does', () => {
        it('adds the parent directory to the project paths', async () => {
          const pathToOpen = path.join(__dirname, 'this-path-does-not-exist.txt')
          await atom.openLocations([{pathToOpen}])
          expect(atom.project.getPaths()[0]).toBe(__dirname)
        })
      })

      describe('when the opened path is a file', () => {
        it('opens it in the workspace', async () => {
          const pathToOpen = __filename
          await atom.openLocations([{pathToOpen}])
          expect(atom.workspace.open.mostRecentCall.args[0]).toBe(__filename)
        })
      })

      describe('when the opened path is a directory', () => {
        it('does not open it in the workspace', async () => {
          const pathToOpen = __dirname
          await atom.openLocations([{pathToOpen}])
          expect(atom.workspace.open.callCount).toBe(0)
        })
      })

      describe('when the opened path is a uri', () => {
        it("adds it to the project's paths as is", async () => {
          const pathToOpen = 'remote://server:7644/some/dir/path'
          spyOn(atom.project, 'addPath')
          await atom.openLocations([{pathToOpen}])
          expect(atom.project.addPath).toHaveBeenCalledWith(pathToOpen)
        })
      })
    })

    describe('when there is saved state for the relevant directories', () => {
      const state = Symbol('savedState')

      beforeEach(() => {
        spyOn(atom, 'getStateKey').andCallFake(dirs => dirs.join(':'))
        spyOn(atom, 'loadState').andCallFake(function (key) {
          if (key === __dirname) { return Promise.resolve(state) } else { return Promise.resolve(null) }
        })
        spyOn(atom, 'attemptRestoreProjectStateForPaths')
      })

      describe('when there are no project folders', () => {
        it('attempts to restore the project state', async () => {
          const pathToOpen = __dirname
          await atom.openLocations([{pathToOpen}])
          expect(atom.attemptRestoreProjectStateForPaths).toHaveBeenCalledWith(state, [pathToOpen], [])
          expect(atom.project.getPaths()).toEqual([])
        })

        it('opens the specified files', async () => {
          await atom.openLocations([{pathToOpen: __dirname}, {pathToOpen: __filename}])
          expect(atom.attemptRestoreProjectStateForPaths).toHaveBeenCalledWith(state, [__dirname], [__filename])
          expect(atom.project.getPaths()).toEqual([])
        })
      })

      describe('when there are already project folders', () => {
        beforeEach(() => atom.project.setPaths([__dirname]))

        it('does not attempt to restore the project state, instead adding the project paths', async () => {
          const pathToOpen = path.join(__dirname, 'fixtures')
          await atom.openLocations([{pathToOpen, forceAddToWindow: true}])
          expect(atom.attemptRestoreProjectStateForPaths).not.toHaveBeenCalled()
          expect(atom.project.getPaths()).toEqual([__dirname, pathToOpen])
        })

        it('opens the specified files', async () => {
          const pathToOpen = path.join(__dirname, 'fixtures')
          const fileToOpen = path.join(pathToOpen, 'michelle-is-awesome.txt')
          await atom.openLocations([{pathToOpen}, {pathToOpen: fileToOpen}])
          expect(atom.attemptRestoreProjectStateForPaths).not.toHaveBeenCalledWith(state, [pathToOpen], [fileToOpen])
          expect(atom.project.getPaths()).toEqual([__dirname])
        })
      })
    })
  })

  describe('::updateAvailable(info) (called via IPC from browser process)', () => {
    let subscription

    afterEach(() => {
      if (subscription) subscription.dispose()
    })

    it('invokes onUpdateAvailable listeners', async () => {
      if (process.platform !== 'darwin') return // Test tied to electron autoUpdater, we use something else on Linux and Win32

      const updateAvailablePromise = new Promise(resolve => {
        subscription = atom.onUpdateAvailable(resolve)
      })

      atom.listenForUpdates()
      const {autoUpdater} = require('electron').remote
      autoUpdater.emit('update-downloaded', null, 'notes', 'version')

      const {releaseVersion} = await updateAvailablePromise
      expect(releaseVersion).toBe('version')
    })
  })

  describe('::getReleaseChannel()', () => {
    let version

    beforeEach(() => {
      spyOn(atom, 'getVersion').andCallFake(() => version)
    })

    it('returns the correct channel based on the version number', () => {
      version = '1.5.6'
      expect(atom.getReleaseChannel()).toBe('stable')

      version = '1.5.0-beta10'
      expect(atom.getReleaseChannel()).toBe('beta')

      version = '1.7.0-dev-5340c91'
      expect(atom.getReleaseChannel()).toBe('dev')
    })
  })
})
