text: const temp = require('temp').track()
  const TextBuffer = require('text-buffer')
  const Project = require('../src/project')
  const fs = require('fs-plus')
  const path = require('path')
  const {Directory} = require('pathwatcher')
  const {stopAllWatchers} = require('../src/path-watcher')
  const GitRepository = require('../src/git-repository')
  
  describe('Project', () => {
    beforeEach(() => {
      const directory = atom.project.getDirectories()[0]
      const paths = directory ? [directory.resolve('dir')] : [null]
      atom.project.setPaths(paths)
  
      // Wait for project's service consumers to be asynchronously added
      waits(1)
    })
  
    describe('serialization', () => {
      let deserializedProject = null
      let notQuittingProject = null
      let quittingProject = null
  
      afterEach(() => {
        if (deserializedProject != null) {
          deserializedProject.destroy()
        }
        if (notQuittingProject != null) {
          notQuittingProject.destroy()
        }
        if (quittingProject != null) {
          quittingProject.destroy()
        }
      })
  
      it("does not deserialize paths to directories that don't exist", () => {
        deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
        const state = atom.project.serialize()
        state.paths.push('/directory/that/does/not/exist')
  
        let err = null
        waitsForPromise(() =>
          deserializedProject.deserialize(state, atom.deserializers)
            .catch(e => { err = e })
        )
  
        runs(() => {
          expect(deserializedProject.getPaths()).toEqual(atom.project.getPaths())
          expect(err.missingProjectPaths).toEqual(['/directory/that/does/not/exist'])
        })
      })
  
      it('does not deserialize paths that are now files', () => {
        const childPath = path.join(temp.mkdirSync('atom-spec-project'), 'child')
        fs.mkdirSync(childPath)
  
        deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
        atom.project.setPaths([childPath])
        const state = atom.project.serialize()
  
        fs.rmdirSync(childPath)
        fs.writeFileSync(childPath, 'surprise!\n')
  
        let err = null
        waitsForPromise(() =>
          deserializedProject.deserialize(state, atom.deserializers)
            .catch(e => { err = e })
        )
  
        runs(() => {
          expect(deserializedProject.getPaths()).toEqual([])
          expect(err.missingProjectPaths).toEqual([childPath])
        })
      })
  
      it('does not include unretained buffers in the serialized state', () => {
        waitsForPromise(() => atom.project.bufferForPath('a'))
  
        runs(() => {
          expect(atom.project.getBuffers().length).toBe(1)
  
          deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
        })
  
        waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false})))
  
        runs(() => expect(deserializedProject.getBuffers().length).toBe(0))
      })
  
      it('listens for destroyed events on deserialized buffers and removes them when they are destroyed', () => {
        waitsForPromise(() => atom.workspace.open('a'))
  
        runs(() => {
          expect(atom.project.getBuffers().length).toBe(1)
          deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
        })
  
        waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false})))
  
        runs(() => {
          expect(deserializedProject.getBuffers().length).toBe(1)
          deserializedProject.getBuffers()[0].destroy()
          expect(deserializedProject.getBuffers().length).toBe(0)
        })
      })
  
      it('does not deserialize buffers when their path is now a directory', () => {
        const pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt')
  
        waitsForPromise(() => atom.workspace.open(pathToOpen))
  
        runs(() => {
          expect(atom.project.getBuffers().length).toBe(1)
          fs.mkdirSync(pathToOpen)
          deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
        })
  
        waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false})))
  
        runs(() => expect(deserializedProject.getBuffers().length).toBe(0))
      })
  
      it('does not deserialize buffers when their path is inaccessible', () => {
        if (process.platform === 'win32') { return } // chmod not supported on win32
        const pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt')
        fs.writeFileSync(pathToOpen, '')
  
        waitsForPromise(() => atom.workspace.open(pathToOpen))
  
        runs(() => {
          expect(atom.project.getBuffers().length).toBe(1)
          fs.chmodSync(pathToOpen, '000')
          deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
        })
  
        waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false})))
  
        runs(() => expect(deserializedProject.getBuffers().length).toBe(0))
      })
  
      it('does not deserialize buffers with their path is no longer present', () => {
        const pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt')
        fs.writeFileSync(pathToOpen, '')
  
        waitsForPromise(() => atom.workspace.open(pathToOpen))
  
        runs(() => {
          expect(atom.project.getBuffers().length).toBe(1)
          fs.unlinkSync(pathToOpen)
          deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
        })
  
        waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false})))
  
        runs(() => expect(deserializedProject.getBuffers().length).toBe(0))
      })
  
      it('deserializes buffers that have never been saved before', () => {
        const pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt')
  
        waitsForPromise(() => atom.workspace.open(pathToOpen))
  
        runs(() => {
          atom.workspace.getActiveTextEditor().setText('unsaved\n')
          expect(atom.project.getBuffers().length).toBe(1)
  
          deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
        })
  
        waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false})))
  
        runs(() => {
          expect(deserializedProject.getBuffers().length).toBe(1)
          expect(deserializedProject.getBuffers()[0].getPath()).toBe(pathToOpen)
          expect(deserializedProject.getBuffers()[0].getText()).toBe('unsaved\n')
        })
      })
  
      it('serializes marker layers and history only if Atom is quitting', () => {
        waitsForPromise(() => atom.workspace.open('a'))
  
        let bufferA = null
        let layerA = null
        let markerA = null
  
        runs(() => {
          bufferA = atom.project.getBuffers()[0]
          layerA = bufferA.addMarkerLayer({persistent: true})
          markerA = layerA.markPosition([0, 3])
          bufferA.append('!')
          notQuittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
        })
  
        waitsForPromise(() => notQuittingProject.deserialize(atom.project.serialize({isUnloading: false})))
  
        runs(() => {
          expect(notQuittingProject.getBuffers()[0].getMarkerLayer(layerA.id), x => x.getMarker(markerA.id)).toBeUndefined()
          expect(notQuittingProject.getBuffers()[0].undo()).toBe(false)
          quittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
        })
  
        waitsForPromise(() => quittingProject.deserialize(atom.project.serialize({isUnloading: true})))
  
        runs(() => {
          expect(quittingProject.getBuffers()[0].getMarkerLayer(layerA.id), x => x.getMarker(markerA.id)).not.toBeUndefined()
          expect(quittingProject.getBuffers()[0].undo()).toBe(true)
        })
      })
    })
  
    describe('when an editor is saved and the project has no path', () =>
      it("sets the project's path to the saved file's parent directory", () => {
        const tempFile = temp.openSync().path
        atom.project.setPaths([])
        expect(atom.project.getPaths()[0]).toBeUndefined()
        let editor = null
  
        waitsForPromise(() => atom.workspace.open().then(o => { editor = o }))
  
        waitsForPromise(() => editor.saveAs(tempFile))
  
        runs(() => expect(atom.project.getPaths()[0]).toBe(path.dirname(tempFile)))
      })
    )
  
    describe('before and after saving a buffer', () => {
      let buffer
      beforeEach(() =>
        waitsForPromise(() =>
          atom.project.bufferForPath(path.join(__dirname, 'fixtures', 'sample.js')).then((o) => {
            buffer = o
            buffer.retain()
          })
        )
      )
  
      afterEach(() => buffer.release())
  
      it('emits save events on the main process', () => {
        spyOn(atom.project.applicationDelegate, 'emitDidSavePath')
        spyOn(atom.project.applicationDelegate, 'emitWillSavePath')
  
        waitsForPromise(() => buffer.save())
  
        runs(() => {
          expect(atom.project.applicationDelegate.emitDidSavePath.calls.length).toBe(1)
          expect(atom.project.applicationDelegate.emitDidSavePath).toHaveBeenCalledWith(buffer.getPath())
          expect(atom.project.applicationDelegate.emitWillSavePath.calls.length).toBe(1)
          expect(atom.project.applicationDelegate.emitWillSavePath).toHaveBeenCalledWith(buffer.getPath())
        })
      })
    })
  
    describe('when a watch error is thrown from the TextBuffer', () => {
      let editor = null
      beforeEach(() =>
        waitsForPromise(() => atom.workspace.open(require.resolve('./fixtures/dir/a')).then(o => { editor = o }))
      )
  
      it('creates a warning notification', () => {
        let noteSpy
        atom.notifications.onDidAddNotification(noteSpy = jasmine.createSpy())
  
        const error = new Error('SomeError')
        error.eventType = 'resurrect'
        editor.buffer.emitter.emit('will-throw-watch-error', {
          handle: jasmine.createSpy(),
          error
        }
        )
  
        expect(noteSpy).toHaveBeenCalled()
  
        const notification = noteSpy.mostRecentCall.args[0]
        expect(notification.getType()).toBe('warning')
        expect(notification.getDetail()).toBe('SomeError')
        expect(notification.getMessage()).toContain('`resurrect`')
        expect(notification.getMessage()).toContain(path.join('fixtures', 'dir', 'a'))
      })
    })
  
    describe('when a custom repository-provider service is provided', () => {
      let fakeRepositoryProvider, fakeRepository
  
      beforeEach(() => {
        fakeRepository = {destroy () { return null }}
        fakeRepositoryProvider = {
          repositoryForDirectory (directory) { return Promise.resolve(fakeRepository) },
          repositoryForDirectorySync (directory) { return fakeRepository }
        }
      })
  
      it('uses it to create repositories for any directories that need one', () => {
        const projectPath = temp.mkdirSync('atom-project')
        atom.project.setPaths([projectPath])
        expect(atom.project.getRepositories()).toEqual([null])
  
        atom.packages.serviceHub.provide('atom.repository-provider', '0.1.0', fakeRepositoryProvider)
        waitsFor(() => atom.project.repositoryProviders.length > 1)
        runs(() => atom.project.getRepositories()[0] === fakeRepository)
      })
  
      it('does not create any new repositories if every directory has a repository', () => {
        const repositories = atom.project.getRepositories()
        expect(repositories.length).toEqual(1)
        expect(repositories[0]).toBeTruthy()
  
        atom.packages.serviceHub.provide('atom.repository-provider', '0.1.0', fakeRepositoryProvider)
        waitsFor(() => atom.project.repositoryProviders.length > 1)
        runs(() => expect(atom.project.getRepositories()).toBe(repositories))
      })
  
      it('stops using it to create repositories when the service is removed', () => {
        atom.project.setPaths([])
  
        const disposable = atom.packages.serviceHub.provide('atom.repository-provider', '0.1.0', fakeRepositoryProvider)
        waitsFor(() => atom.project.repositoryProviders.length > 1)
        runs(() => {
          disposable.dispose()
          atom.project.addPath(temp.mkdirSync('atom-project'))
          expect(atom.project.getRepositories()).toEqual([null])
        })
      })
    })
  
    describe('when a custom directory-provider service is provided', () => {
      class DummyDirectory {
        constructor (aPath) {
          this.path = aPath
        }
        getPath () { return this.path }
        getFile () { return {existsSync () { return false }} }
        getSubdirectory () { return {existsSync () { return false }} }
        isRoot () { return true }
        existsSync () { return this.path.endsWith('does-exist') }
        contains (filePath) { return filePath.startsWith(this.path) }
        onDidChangeFiles (callback) {
          onDidChangeFilesCallback = callback
          return {dispose: () => {}}
        }
      }
  
      let serviceDisposable = null
      let onDidChangeFilesCallback = null
  
      beforeEach(() => {
        serviceDisposable = atom.packages.serviceHub.provide('atom.directory-provider', '0.1.0', {
          directoryForURISync (uri) {
            if (uri.startsWith('ssh://')) {
              return new DummyDirectory(uri)
            } else {
              return null
            }
          }
        })
        onDidChangeFilesCallback = null
  
        waitsFor(() => atom.project.directoryProviders.length > 0)
      })
  
      it("uses the provider's custom directories for any paths that it handles", () => {
        const localPath = temp.mkdirSync('local-path')
        const remotePath = 'ssh://foreign-directory:8080/does-exist'
  
        atom.project.setPaths([localPath, remotePath])
  
        let directories = atom.project.getDirectories()
        expect(directories[0].getPath()).toBe(localPath)
        expect(directories[0] instanceof Directory).toBe(true)
        expect(directories[1].getPath()).toBe(remotePath)
        expect(directories[1] instanceof DummyDirectory).toBe(true)
  
        // It does not add new remote paths that do not exist
        const nonExistentRemotePath = 'ssh://another-directory:8080/does-not-exist'
        atom.project.addPath(nonExistentRemotePath)
        expect(atom.project.getDirectories().length).toBe(2)
  
        // It adds new remote paths if their directories exist.
        const newRemotePath = 'ssh://another-directory:8080/does-exist'
        atom.project.addPath(newRemotePath)
        directories = atom.project.getDirectories()
        expect(directories[2].getPath()).toBe(newRemotePath)
        expect(directories[2] instanceof DummyDirectory).toBe(true)
      })
  
      it('stops using the provider when the service is removed', () => {
        serviceDisposable.dispose()
        atom.project.setPaths(['ssh://foreign-directory:8080/does-exist'])
        expect(atom.project.getDirectories().length).toBe(0)
      })
  
      it('uses the custom onDidChangeFiles as the watcher if available', () => {
        // Ensure that all preexisting watchers are stopped
        waitsForPromise(() => stopAllWatchers())
  
        const remotePath = 'ssh://another-directory:8080/does-exist'
        runs(() => atom.project.setPaths([remotePath]))
        waitsForPromise(() => atom.project.getWatcherPromise(remotePath))
  
        runs(() => {
          expect(onDidChangeFilesCallback).not.toBeNull()
  
          const changeSpy = jasmine.createSpy('atom.project.onDidChangeFiles')
          const disposable = atom.project.onDidChangeFiles(changeSpy)
  
          const events = [{action: 'created', path: remotePath + '/test.txt'}]
          onDidChangeFilesCallback(events)
  
          expect(changeSpy).toHaveBeenCalledWith(events)
          disposable.dispose()
        })
      })
    })
  
    describe('.open(path)', () => {
      let absolutePath, newBufferHandler
  
      beforeEach(() => {
        absolutePath = require.resolve('./fixtures/dir/a')
        newBufferHandler = jasmine.createSpy('newBufferHandler')
        atom.project.onDidAddBuffer(newBufferHandler)
      })
  
      describe("when given an absolute path that isn't currently open", () =>
        it("returns a new edit session for the given path and emits 'buffer-created'", () => {
          let editor = null
          waitsForPromise(() => atom.workspace.open(absolutePath).then(o => { editor = o }))
  
          runs(() => {
            expect(editor.buffer.getPath()).toBe(absolutePath)
            expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer)
          })
        })
      )
  
      describe("when given a relative path that isn't currently opened", () =>
        it("returns a new edit session for the given path (relative to the project root) and emits 'buffer-created'", () => {
          let editor = null
          waitsForPromise(() => atom.workspace.open(absolutePath).then(o => { editor = o }))
  
          runs(() => {
            expect(editor.buffer.getPath()).toBe(absolutePath)
            expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer)
          })
        })
      )
  
      describe('when passed the path to a buffer that is currently opened', () =>
        it('returns a new edit session containing currently opened buffer', () => {
          let editor = null
  
          waitsForPromise(() => atom.workspace.open(absolutePath).then(o => { editor = o }))
  
          runs(() => newBufferHandler.reset())
  
          waitsForPromise(() =>
            atom.workspace.open(absolutePath).then(({buffer}) => expect(buffer).toBe(editor.buffer))
          )
  
          waitsForPromise(() =>
            atom.workspace.open('a').then(({buffer}) => {
              expect(buffer).toBe(editor.buffer)
              expect(newBufferHandler).not.toHaveBeenCalled()
            })
          )
        })
      )
  
      describe('when not passed a path', () =>
        it("returns a new edit session and emits 'buffer-created'", () => {
          let editor = null
          waitsForPromise(() => atom.workspace.open().then(o => { editor = o }))
  
          runs(() => {
            expect(editor.buffer.getPath()).toBeUndefined()
            expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer)
          })
        })
      )
    })
  
    describe('.bufferForPath(path)', () => {
      let buffer = null
  
      beforeEach(() =>
        waitsForPromise(() =>
          atom.project.bufferForPath('a').then((o) => {
            buffer = o
            buffer.retain()
          })
        )
      )
  
      afterEach(() => buffer.release())
  
      describe('when opening a previously opened path', () => {
        it('does not create a new buffer', () => {
          waitsForPromise(() =>
            atom.project.bufferForPath('a').then(anotherBuffer => expect(anotherBuffer).toBe(buffer))
          )
  
          waitsForPromise(() =>
            atom.project.bufferForPath('b').then(anotherBuffer => expect(anotherBuffer).not.toBe(buffer))
          )
  
          waitsForPromise(() =>
            Promise.all([
              atom.project.bufferForPath('c'),
              atom.project.bufferForPath('c')
            ]).then(([buffer1, buffer2]) => {
              expect(buffer1).toBe(buffer2)
            })
          )
        })
  
        it('retries loading the buffer if it previously failed', () => {
          waitsForPromise({shouldReject: true}, () => {
            spyOn(TextBuffer, 'load').andCallFake(() => Promise.reject(new Error('Could not open file')))
            return atom.project.bufferForPath('b')
          })
  
          waitsForPromise({shouldReject: false}, () => {
            TextBuffer.load.andCallThrough()
            return atom.project.bufferForPath('b')
          })
        })
  
        it('creates a new buffer if the previous buffer was destroyed', () => {
          buffer.release()
  
          waitsForPromise(() =>
            atom.project.bufferForPath('b').then(anotherBuffer => expect(anotherBuffer).not.toBe(buffer))
          )
        })
      })
    })
  
    describe('.repositoryForDirectory(directory)', () => {
      it('resolves to null when the directory does not have a repository', () =>
        waitsForPromise(() => {
          const directory = new Directory('/tmp')
          return atom.project.repositoryForDirectory(directory).then((result) => {
            expect(result).toBeNull()
            expect(atom.project.repositoryProviders.length).toBeGreaterThan(0)
            expect(atom.project.repositoryPromisesByPath.size).toBe(0)
          })
        })
      )
  
      it('resolves to a GitRepository and is cached when the given directory is a Git repo', () =>
        waitsForPromise(() => {
          const directory = new Directory(path.join(__dirname, '..'))
          const promise = atom.project.repositoryForDirectory(directory)
          return promise.then((result) => {
            expect(result).toBeInstanceOf(GitRepository)
            const dirPath = directory.getRealPathSync()
            expect(result.getPath()).toBe(path.join(dirPath, '.git'))
  
            // Verify that the result is cached.
            expect(atom.project.repositoryForDirectory(directory)).toBe(promise)
          })
        })
      )
  
      it('creates a new repository if a previous one with the same directory had been destroyed', () => {
        let repository = null
        const directory = new Directory(path.join(__dirname, '..'))
  
        waitsForPromise(() => atom.project.repositoryForDirectory(directory).then(repo => { repository = repo }))
  
        runs(() => {
          expect(repository.isDestroyed()).toBe(false)
          repository.destroy()
          expect(repository.isDestroyed()).toBe(true)
        })
  
        waitsForPromise(() => atom.project.repositoryForDirectory(directory).then(repo => { repository = repo }))
  
        runs(() => expect(repository.isDestroyed()).toBe(false))
      })
    })
  
    describe('.setPaths(paths, options)', () => {
      describe('when path is a file', () =>
        it("sets its path to the file's parent directory and updates the root directory", () => {
          const filePath = require.resolve('./fixtures/dir/a')
          atom.project.setPaths([filePath])
          expect(atom.project.getPaths()[0]).toEqual(path.dirname(filePath))
          expect(atom.project.getDirectories()[0].path).toEqual(path.dirname(filePath))
        })
      )
  
      describe('when path is a directory', () => {
        it('assigns the directories and repositories', () => {
          const directory1 = temp.mkdirSync('non-git-repo')
          const directory2 = temp.mkdirSync('git-repo1')
          const directory3 = temp.mkdirSync('git-repo2')
  
          const gitDirPath = fs.absolute(path.join(__dirname, 'fixtures', 'git', 'master.git'))
          fs.copySync(gitDirPath, path.join(directory2, '.git'))
          fs.copySync(gitDirPath, path.join(directory3, '.git'))
  
          atom.project.setPaths([directory1, directory2, directory3])
  
          const [repo1, repo2, repo3] = atom.project.getRepositories()
          expect(repo1).toBeNull()
          expect(repo2.getShortHead()).toBe('master')
          expect(repo2.getPath()).toBe(fs.realpathSync(path.join(directory2, '.git')))
          expect(repo3.getShortHead()).toBe('master')
          expect(repo3.getPath()).toBe(fs.realpathSync(path.join(directory3, '.git')))
        })
  
        it('calls callbacks registered with ::onDidChangePaths', () => {
          const onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy')
          atom.project.onDidChangePaths(onDidChangePathsSpy)
  
          const paths = [ temp.mkdirSync('dir1'), temp.mkdirSync('dir2') ]
          atom.project.setPaths(paths)
  
          expect(onDidChangePathsSpy.callCount).toBe(1)
          expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual(paths)
        })
  
        it('optionally throws an error with any paths that did not exist', () => {
          const paths = [temp.mkdirSync('exists0'), '/doesnt-exists/0', temp.mkdirSync('exists1'), '/doesnt-exists/1']
  
          try {
            atom.project.setPaths(paths, {mustExist: true})
            expect('no exception thrown').toBeUndefined()
          } catch (e) {
            expect(e.missingProjectPaths).toEqual([paths[1], paths[3]])
          }
  
          expect(atom.project.getPaths()).toEqual([paths[0], paths[2]])
        })
      })
  
      describe('when no paths are given', () =>
        it('clears its path', () => {
          atom.project.setPaths([])
          expect(atom.project.getPaths()).toEqual([])
          expect(atom.project.getDirectories()).toEqual([])
        })
    )
  
      it('normalizes the path to remove consecutive slashes, ., and .. segments', () => {
        atom.project.setPaths([`${require.resolve('./fixtures/dir/a')}${path.sep}b${path.sep}${path.sep}..`])
        expect(atom.project.getPaths()[0]).toEqual(path.dirname(require.resolve('./fixtures/dir/a')))
        expect(atom.project.getDirectories()[0].path).toEqual(path.dirname(require.resolve('./fixtures/dir/a')))
      })
    })
  
    describe('.addPath(path, options)', () => {
      it('calls callbacks registered with ::onDidChangePaths', () => {
        const onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy')
        atom.project.onDidChangePaths(onDidChangePathsSpy)
  
        const [oldPath] = atom.project.getPaths()
  
        const newPath = temp.mkdirSync('dir')
        atom.project.addPath(newPath)
  
        expect(onDidChangePathsSpy.callCount).toBe(1)
        expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual([oldPath, newPath])
      })
  
      it("doesn't add redundant paths", () => {
        const onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy')
        atom.project.onDidChangePaths(onDidChangePathsSpy)
        const [oldPath] = atom.project.getPaths()
  
        // Doesn't re-add an existing root directory
        atom.project.addPath(oldPath)
        expect(atom.project.getPaths()).toEqual([oldPath])
        expect(onDidChangePathsSpy).not.toHaveBeenCalled()
  
        // Doesn't add an entry for a file-path within an existing root directory
        atom.project.addPath(path.join(oldPath, 'some-file.txt'))
        expect(atom.project.getPaths()).toEqual([oldPath])
        expect(onDidChangePathsSpy).not.toHaveBeenCalled()
  
        // Does add an entry for a directory within an existing directory
        const newPath = path.join(oldPath, 'a-dir')
        atom.project.addPath(newPath)
        expect(atom.project.getPaths()).toEqual([oldPath, newPath])
        expect(onDidChangePathsSpy).toHaveBeenCalled()
      })
  
      it("doesn't add non-existent directories", () => {
        const previousPaths = atom.project.getPaths()
        atom.project.addPath('/this-definitely/does-not-exist')
        expect(atom.project.getPaths()).toEqual(previousPaths)
      })
  
      it('optionally throws on non-existent directories', () =>
        expect(() => atom.project.addPath('/this-definitely/does-not-exist', {mustExist: true})).toThrow()
      )
    })
  
    describe('.removePath(path)', () => {
      let onDidChangePathsSpy = null
  
      beforeEach(() => {
        onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths listener')
        atom.project.onDidChangePaths(onDidChangePathsSpy)
      })
  
      it('removes the directory and repository for the path', () => {
        const result = atom.project.removePath(atom.project.getPaths()[0])
        expect(atom.project.getDirectories()).toEqual([])
        expect(atom.project.getRepositories()).toEqual([])
        expect(atom.project.getPaths()).toEqual([])
        expect(result).toBe(true)
        expect(onDidChangePathsSpy).toHaveBeenCalled()
      })
  
      it("does nothing if the path is not one of the project's root paths", () => {
        const originalPaths = atom.project.getPaths()
        const result = atom.project.removePath(originalPaths[0] + 'xyz')
        expect(result).toBe(false)
        expect(atom.project.getPaths()).toEqual(originalPaths)
        expect(onDidChangePathsSpy).not.toHaveBeenCalled()
      })
  
      it("doesn't destroy the repository if it is shared by another root directory", () => {
        atom.project.setPaths([__dirname, path.join(__dirname, '..', 'src')])
        atom.project.removePath(__dirname)
        expect(atom.project.getPaths()).toEqual([path.join(__dirname, '..', 'src')])
        expect(atom.project.getRepositories()[0].isSubmodule('src')).toBe(false)
      })
  
      it('removes a path that is represented as a URI', () => {
        atom.packages.serviceHub.provide('atom.directory-provider', '0.1.0', {
          directoryForURISync (uri) {
            return {
              getPath () { return uri },
              getSubdirectory () { return {} },
              isRoot () { return true },
              existsSync () { return true },
              off () {}
            }
          }
        })
  
        const ftpURI = 'ftp://example.com/some/folder'
  
        atom.project.setPaths([ftpURI])
        expect(atom.project.getPaths()).toEqual([ftpURI])
  
        atom.project.removePath(ftpURI)
        expect(atom.project.getPaths()).toEqual([])
      })
    })
  
    describe('.onDidChangeFiles()', () => {
      let sub = []
      const events = []
      let checkCallback = () => {}
  
      beforeEach(() => {
        sub = atom.project.onDidChangeFiles((incoming) => {
          events.push(...incoming)
          checkCallback()
        })
      })
  
      afterEach(() => sub.dispose())
  
      const waitForEvents = (paths) => {
        const remaining = new Set(paths.map((p) => fs.realpathSync(p)))
        return new Promise((resolve, reject) => {
          checkCallback = () => {
            for (let event of events) { remaining.delete(event.path) }
            if (remaining.size === 0) { resolve() }
          }
  
          const expire = () => {
            checkCallback = () => {}
            console.error('Paths not seen:', remaining)
            reject(new Error('Expired before all expected events were delivered.'))
          }
  
          checkCallback()
          setTimeout(expire, 2000)
        })
      }
  
      it('reports filesystem changes within project paths', () => {
        const dirOne = temp.mkdirSync('atom-spec-project-one')
        const fileOne = path.join(dirOne, 'file-one.txt')
        const fileTwo = path.join(dirOne, 'file-two.txt')
        const dirTwo = temp.mkdirSync('atom-spec-project-two')
        const fileThree = path.join(dirTwo, 'file-three.txt')
  
        // Ensure that all preexisting watchers are stopped
        waitsForPromise(() => stopAllWatchers())
  
        runs(() => atom.project.setPaths([dirOne]))
        waitsForPromise(() => atom.project.getWatcherPromise(dirOne))
  
        runs(() => {
          expect(atom.project.watcherPromisesByPath[dirTwo]).toEqual(undefined)
  
          fs.writeFileSync(fileThree, 'three\n')
          fs.writeFileSync(fileTwo, 'two\n')
          fs.writeFileSync(fileOne, 'one\n')
        })
  
        waitsForPromise(() => waitForEvents([fileOne, fileTwo]))
  
        runs(() => expect(events.some(event => event.path === fileThree)).toBeFalsy())
      })
    })
  
    describe('.onDidAddBuffer()', () =>
      it('invokes the callback with added text buffers', () => {
        const buffers = []
        const added = []
  
        waitsForPromise(() =>
          atom.project.buildBuffer(require.resolve('./fixtures/dir/a'))
            .then(o => buffers.push(o))
        )
  
        runs(() => {
          expect(buffers.length).toBe(1)
          atom.project.onDidAddBuffer(buffer => added.push(buffer))
        })
  
        waitsForPromise(() =>
          atom.project.buildBuffer(require.resolve('./fixtures/dir/b'))
            .then(o => buffers.push(o))
        )
  
        runs(() => {
          expect(buffers.length).toBe(2)
          expect(added).toEqual([buffers[1]])
        })
      })
  )
  
    describe('.observeBuffers()', () =>
      it('invokes the observer with current and future text buffers', () => {
        const buffers = []
        const observed = []
  
        waitsForPromise(() =>
          atom.project.buildBuffer(require.resolve('./fixtures/dir/a'))
            .then(o => buffers.push(o))
        )
  
        waitsForPromise(() =>
          atom.project.buildBuffer(require.resolve('./fixtures/dir/b'))
            .then(o => buffers.push(o))
        )
  
        runs(() => {
          expect(buffers.length).toBe(2)
          atom.project.observeBuffers(buffer => observed.push(buffer))
          expect(observed).toEqual(buffers)
        })
  
        waitsForPromise(() =>
          atom.project.buildBuffer(require.resolve('./fixtures/dir/b'))
            .then(o => buffers.push(o))
        )
  
        runs(() => {
          expect(observed.length).toBe(3)
          expect(buffers.length).toBe(3)
          expect(observed).toEqual(buffers)
        })
      })
    )
  
    describe('.relativize(path)', () => {
      it('returns the path, relative to whichever root directory it is inside of', () => {
        atom.project.addPath(temp.mkdirSync('another-path'))
  
        let rootPath = atom.project.getPaths()[0]
        let childPath = path.join(rootPath, 'some', 'child', 'directory')
        expect(atom.project.relativize(childPath)).toBe(path.join('some', 'child', 'directory'))
  
        rootPath = atom.project.getPaths()[1]
        childPath = path.join(rootPath, 'some', 'child', 'directory')
        expect(atom.project.relativize(childPath)).toBe(path.join('some', 'child', 'directory'))
      })
  
      it('returns the given path if it is not in any of the root directories', () => {
        const randomPath = path.join('some', 'random', 'path')
        expect(atom.project.relativize(randomPath)).toBe(randomPath)
      })
    })
  
    describe('.relativizePath(path)', () => {
      it('returns the root path that contains the given path, and the path relativized to that root path', () => {
        atom.project.addPath(temp.mkdirSync('another-path'))
  
        let rootPath = atom.project.getPaths()[0]
        let childPath = path.join(rootPath, 'some', 'child', 'directory')
        expect(atom.project.relativizePath(childPath)).toEqual([rootPath, path.join('some', 'child', 'directory')])
  
        rootPath = atom.project.getPaths()[1]
        childPath = path.join(rootPath, 'some', 'child', 'directory')
        expect(atom.project.relativizePath(childPath)).toEqual([rootPath, path.join('some', 'child', 'directory')])
      })
  
      describe("when the given path isn't inside of any of the project's path", () =>
        it('returns null for the root path, and the given path unchanged', () => {
          const randomPath = path.join('some', 'random', 'path')
          expect(atom.project.relativizePath(randomPath)).toEqual([null, randomPath])
        })
    )
  
      describe('when the given path is a URL', () =>
        it('returns null for the root path, and the given path unchanged', () => {
          const url = 'http://the-path'
          expect(atom.project.relativizePath(url)).toEqual([null, url])
        })
    )
  
      describe('when the given path is inside more than one root folder', () =>
        it('uses the root folder that is closest to the given path', () => {
          atom.project.addPath(path.join(atom.project.getPaths()[0], 'a-dir'))
  
          const inputPath = path.join(atom.project.getPaths()[1], 'somewhere/something.txt')
  
          expect(atom.project.getDirectories()[0].contains(inputPath)).toBe(true)
          expect(atom.project.getDirectories()[1].contains(inputPath)).toBe(true)
          expect(atom.project.relativizePath(inputPath)).toEqual([
            atom.project.getPaths()[1],
            path.join('somewhere', 'something.txt')
          ])
        })
    )
    })
  
    describe('.contains(path)', () =>
      it('returns whether or not the given path is in one of the root directories', () => {
        const rootPath = atom.project.getPaths()[0]
        const childPath = path.join(rootPath, 'some', 'child', 'directory')
        expect(atom.project.contains(childPath)).toBe(true)
  
        const randomPath = path.join('some', 'random', 'path')
        expect(atom.project.contains(randomPath)).toBe(false)
      })
    )
  
    describe('.resolvePath(uri)', () =>
      it('normalizes disk drive letter in passed path on #win32', () => {
        expect(atom.project.resolvePath('d:\\file.txt')).toEqual('D:\\file.txt')
      })
    )
  })
  