/** @babel */

import path from 'path'
import { Emitter } from 'event-kit'

import { NativeWatcherRegistry } from '../src/native-watcher-registry'

function findRootDirectory () {
  let current = process.cwd()
  while (true) {
    let next = path.resolve(current, '..')
    if (next === current) {
      return next
    } else {
      current = next
    }
  }
}
const ROOT = findRootDirectory()

function absolute (...parts) {
  const candidate = path.join(...parts)
  return path.isAbsolute(candidate) ? candidate : path.join(ROOT, candidate)
}

function parts (fullPath) {
  return fullPath.split(path.sep).filter(part => part.length > 0)
}

class MockWatcher {
  constructor (normalizedPath) {
    this.normalizedPath = normalizedPath
    this.native = null
  }

  getNormalizedPathPromise () {
    return Promise.resolve(this.normalizedPath)
  }

  attachToNative (native, nativePath) {
    if (this.normalizedPath.startsWith(nativePath)) {
      if (this.native) {
        this.native.attached = this.native.attached.filter(
          each => each !== this
        )
      }
      this.native = native
      this.native.attached.push(this)
    }
  }
}

class MockNative {
  constructor (name) {
    this.name = name
    this.attached = []
    this.disposed = false
    this.stopped = false

    this.emitter = new Emitter()
  }

  reattachTo (newNative, nativePath) {
    for (const watcher of this.attached) {
      watcher.attachToNative(newNative, nativePath)
    }
  }

  onWillStop (callback) {
    return this.emitter.on('will-stop', callback)
  }

  dispose () {
    this.disposed = true
  }

  stop () {
    this.stopped = true
    this.emitter.emit('will-stop')
  }
}

describe('NativeWatcherRegistry', function () {
  let createNative, registry

  beforeEach(function () {
    registry = new NativeWatcherRegistry(normalizedPath =>
      createNative(normalizedPath)
    )
  })

  it('attaches a Watcher to a newly created NativeWatcher for a new directory', async function () {
    const watcher = new MockWatcher(absolute('some', 'path'))
    const NATIVE = new MockNative('created')
    createNative = () => NATIVE

    await registry.attach(watcher)

    expect(watcher.native).toBe(NATIVE)
  })

  it('reuses an existing NativeWatcher on the same directory', async function () {
    this.RETRY_FLAKY_TEST_AND_SLOW_DOWN_THE_BUILD()

    const EXISTING = new MockNative('existing')
    const existingPath = absolute('existing', 'path')
    let firstTime = true
    createNative = () => {
      if (firstTime) {
        firstTime = false
        return EXISTING
      }

      return new MockNative('nope')
    }
    await registry.attach(new MockWatcher(existingPath))

    const watcher = new MockWatcher(existingPath)
    await registry.attach(watcher)

    expect(watcher.native).toBe(EXISTING)
  })

  it('attaches to an existing NativeWatcher on a parent directory', async function () {
    const EXISTING = new MockNative('existing')
    const parentDir = absolute('existing', 'path')
    const subDir = path.join(parentDir, 'sub', 'directory')
    let firstTime = true
    createNative = () => {
      if (firstTime) {
        firstTime = false
        return EXISTING
      }

      return new MockNative('nope')
    }
    await registry.attach(new MockWatcher(parentDir))

    const watcher = new MockWatcher(subDir)
    await registry.attach(watcher)

    expect(watcher.native).toBe(EXISTING)
  })

  it('adopts Watchers from NativeWatchers on child directories', async function () {
    const parentDir = absolute('existing', 'path')
    const childDir0 = path.join(parentDir, 'child', 'directory', 'zero')
    const childDir1 = path.join(parentDir, 'child', 'directory', 'one')
    const otherDir = absolute('another', 'path')

    const CHILD0 = new MockNative('existing0')
    const CHILD1 = new MockNative('existing1')
    const OTHER = new MockNative('existing2')
    const PARENT = new MockNative('parent')

    createNative = dir => {
      if (dir === childDir0) {
        return CHILD0
      } else if (dir === childDir1) {
        return CHILD1
      } else if (dir === otherDir) {
        return OTHER
      } else if (dir === parentDir) {
        return PARENT
      } else {
        throw new Error(`Unexpected path: ${dir}`)
      }
    }

    const watcher0 = new MockWatcher(childDir0)
    await registry.attach(watcher0)

    const watcher1 = new MockWatcher(childDir1)
    await registry.attach(watcher1)

    const watcher2 = new MockWatcher(otherDir)
    await registry.attach(watcher2)

    expect(watcher0.native).toBe(CHILD0)
    expect(watcher1.native).toBe(CHILD1)
    expect(watcher2.native).toBe(OTHER)

    // Consolidate all three watchers beneath the same native watcher on the parent directory
    const watcher = new MockWatcher(parentDir)
    await registry.attach(watcher)

    expect(watcher.native).toBe(PARENT)

    expect(watcher0.native).toBe(PARENT)
    expect(CHILD0.stopped).toBe(true)
    expect(CHILD0.disposed).toBe(true)

    expect(watcher1.native).toBe(PARENT)
    expect(CHILD1.stopped).toBe(true)
    expect(CHILD1.disposed).toBe(true)

    expect(watcher2.native).toBe(OTHER)
    expect(OTHER.stopped).toBe(false)
    expect(OTHER.disposed).toBe(false)
  })

  describe('removing NativeWatchers', function () {
    it('happens when they stop', async function () {
      const STOPPED = new MockNative('stopped')
      const RUNNING = new MockNative('running')

      const stoppedPath = absolute('watcher', 'that', 'will', 'be', 'stopped')
      const stoppedPathParts = stoppedPath
        .split(path.sep)
        .filter(part => part.length > 0)
      const runningPath = absolute(
        'watcher',
        'that',
        'will',
        'continue',
        'to',
        'exist'
      )
      const runningPathParts = runningPath
        .split(path.sep)
        .filter(part => part.length > 0)

      createNative = dir => {
        if (dir === stoppedPath) {
          return STOPPED
        } else if (dir === runningPath) {
          return RUNNING
        } else {
          throw new Error(`Unexpected path: ${dir}`)
        }
      }

      const stoppedWatcher = new MockWatcher(stoppedPath)
      await registry.attach(stoppedWatcher)

      const runningWatcher = new MockWatcher(runningPath)
      await registry.attach(runningWatcher)

      STOPPED.stop()

      const runningNode = registry.tree.root.lookup(runningPathParts).when({
        parent: node => node,
        missing: () => false,
        children: () => false
      })
      expect(runningNode).toBeTruthy()
      expect(runningNode.getNativeWatcher()).toBe(RUNNING)

      const stoppedNode = registry.tree.root.lookup(stoppedPathParts).when({
        parent: () => false,
        missing: () => true,
        children: () => false
      })
      expect(stoppedNode).toBe(true)
    })

    it('reassigns new child watchers when a parent watcher is stopped', async function () {
      const CHILD0 = new MockNative('child0')
      const CHILD1 = new MockNative('child1')
      const PARENT = new MockNative('parent')

      const parentDir = absolute('parent')
      const childDir0 = path.join(parentDir, 'child0')
      const childDir1 = path.join(parentDir, 'child1')

      createNative = dir => {
        if (dir === parentDir) {
          return PARENT
        } else if (dir === childDir0) {
          return CHILD0
        } else if (dir === childDir1) {
          return CHILD1
        } else {
          throw new Error(`Unexpected directory ${dir}`)
        }
      }

      const parentWatcher = new MockWatcher(parentDir)
      const childWatcher0 = new MockWatcher(childDir0)
      const childWatcher1 = new MockWatcher(childDir1)

      await registry.attach(parentWatcher)
      await Promise.all([
        registry.attach(childWatcher0),
        registry.attach(childWatcher1)
      ])

      // All three watchers should share the parent watcher's native watcher.
      expect(parentWatcher.native).toBe(PARENT)
      expect(childWatcher0.native).toBe(PARENT)
      expect(childWatcher1.native).toBe(PARENT)

      // Stopping the parent should detach and recreate the child watchers.
      PARENT.stop()

      expect(childWatcher0.native).toBe(CHILD0)
      expect(childWatcher1.native).toBe(CHILD1)

      expect(
        registry.tree.root.lookup(parts(parentDir)).when({
          parent: () => false,
          missing: () => false,
          children: () => true
        })
      ).toBe(true)

      expect(
        registry.tree.root.lookup(parts(childDir0)).when({
          parent: () => true,
          missing: () => false,
          children: () => false
        })
      ).toBe(true)

      expect(
        registry.tree.root.lookup(parts(childDir1)).when({
          parent: () => true,
          missing: () => false,
          children: () => false
        })
      ).toBe(true)
    })

    it('consolidates children when splitting a parent watcher', async function () {
      const CHILD0 = new MockNative('child0')
      const PARENT = new MockNative('parent')

      const parentDir = absolute('parent')
      const childDir0 = path.join(parentDir, 'child0')
      const childDir1 = path.join(parentDir, 'child0', 'child1')

      createNative = dir => {
        if (dir === parentDir) {
          return PARENT
        } else if (dir === childDir0) {
          return CHILD0
        } else {
          throw new Error(`Unexpected directory ${dir}`)
        }
      }

      const parentWatcher = new MockWatcher(parentDir)
      const childWatcher0 = new MockWatcher(childDir0)
      const childWatcher1 = new MockWatcher(childDir1)

      await registry.attach(parentWatcher)
      await Promise.all([
        registry.attach(childWatcher0),
        registry.attach(childWatcher1)
      ])

      // All three watchers should share the parent watcher's native watcher.
      expect(parentWatcher.native).toBe(PARENT)
      expect(childWatcher0.native).toBe(PARENT)
      expect(childWatcher1.native).toBe(PARENT)

      // Stopping the parent should detach and create the child watchers. Both child watchers should
      // share the same native watcher.
      PARENT.stop()

      expect(childWatcher0.native).toBe(CHILD0)
      expect(childWatcher1.native).toBe(CHILD0)

      expect(
        registry.tree.root.lookup(parts(parentDir)).when({
          parent: () => false,
          missing: () => false,
          children: () => true
        })
      ).toBe(true)

      expect(
        registry.tree.root.lookup(parts(childDir0)).when({
          parent: () => true,
          missing: () => false,
          children: () => false
        })
      ).toBe(true)

      expect(
        registry.tree.root.lookup(parts(childDir1)).when({
          parent: () => true,
          missing: () => false,
          children: () => false
        })
      ).toBe(true)
    })
  })
})
