/* eslint-env mocha */

const mineflayer = require('mineflayer')
const { goals, pathfinder, Movements } = require('mineflayer-pathfinder')
const { Vec3 } = require('vec3')
const mc = require('minecraft-protocol')
const assert = require('assert')
const { v4: uuidv4 } = require('uuid')
const PEntity = require('prismarine-entity')
const { once, on } = require('events')
const { Schematic } = require('prismarine-schematic')
const { promises: fs } = require('fs')
const path = require('path')
const Physics = require('../lib/physics')

const Version = '1.16.5'
const ServerPort = 25567

/**
 * Returns a flat bedrock chunk with a single gold block in it.
 * @param {string} Version version
 * @returns {import('prismarine-chunk').Chunk}
 */
function flatMap (Version) {
  const targetBlock = new Vec3(12, 1, 8) // a gold block away from the spawn position
  const Block = require('prismarine-block')(Version)
  const Chunk = require('prismarine-chunk')(Version)
  const mcData = require('minecraft-data')(Version)
  const chunk = new Chunk()
  chunk.initialize((x, y, z) => {
    if (targetBlock.x === x && targetBlock.y === y && targetBlock.z === z) {
      return new Block(mcData.blocksByName.gold_block.id, 1, 0)
    }
    return y === 0 ? new Block(mcData.blocksByName.bedrock.id, 1, 0) : new Block(mcData.blocksByName.air.id, 1, 0) // Bedrock floor
  })
  return chunk
}

/**
 * Reads the schematic parkour1.schem and returns a chunk containing the schematic content.
 * @param {string} Version version to be used
 * @returns {Promise<import('prismarine-chunk').Chunk>}
 */
async function parkourMap (Version) {
  const pwd = path.join(__dirname, './schematics/parkour1.schem')
  const readSchem = await Schematic.read(await fs.readFile(pwd), '1.18.2')
  const Block = require('prismarine-block')(Version)
  const Chunk = require('prismarine-chunk')(Version)
  const mcData = require('minecraft-data')(Version)
  const chunk = new Chunk()
  chunk.initialize((x, y, z) => {
    const block = readSchem.getBlock(new Vec3(x, y, z))
    if (block.name === 'air') return null
    // Different versions off schematic are not compatible with each other. Assumes block names between versions stay the same.
    const blockVersion = mcData.blocksByName[block.name]
    if (!blockVersion) return null
    return new Block(blockVersion.id, 1, 0)
  })
  return chunk
}

function generateChunkPacket (chunk) {
  const lights = chunk.dumpLight()
  return {
    x: 0,
    z: 0,
    groundUp: true,
    biomes: chunk.dumpBiomes !== undefined ? chunk.dumpBiomes() : undefined,
    heightmaps: {
      type: 'compound',
      name: '',
      value: {
        MOTION_BLOCKING: { type: 'longArray', value: new Array(36).fill([0, 0]) }
      }
    }, // send fake heightmap
    bitMap: chunk.getMask(),
    chunkData: chunk.dump(),
    blockEntities: [],
    trustEdges: false,
    skyLightMask: lights?.skyLightMask,
    blockLightMask: lights?.blockLightMask,
    emptySkyLightMask: lights?.emptySkyLightMask,
    emptyBlockLightMask: lights?.emptyBlockLightMask,
    skyLight: lights?.skyLight,
    blockLight: lights?.blockLight
  }
}

/**
 * Create a new 1.16 server and handle when clients connect.
 * @param {import('minecraft-protocol').Server} server
 * @param {import('vec3').Vec3} spawnPos
 * @param {string} Version
 * @param {boolean} useLoginPacket
 * @returns {Promise<void>}
 */
async function newServer (server, chunk, spawnPos, Version, useLoginPacket) {
  const mcData = require('minecraft-data')(Version)
  server = mc.createServer({
    'online-mode': false,
    version: Version,
    // 25565 - local server, 25566 - proxy server
    port: ServerPort
  })
  server.on('login', (client) => {
    let loginPacket
    if (useLoginPacket) {
      loginPacket = mcData.loginPacket
    } else {
      loginPacket = {
        entityId: 0,
        levelType: 'fogetaboutit',
        gameMode: 0,
        previousGameMode: 255,
        worldNames: ['minecraft:overworld'],
        dimension: 0,
        worldName: 'minecraft:overworld',
        hashedSeed: [0, 0],
        difficulty: 0,
        maxPlayers: 20,
        reducedDebugInfo: 1,
        enableRespawnScreen: true
      }
    }

    client.write('login', loginPacket)

    client.write('map_chunk', generateChunkPacket(chunk))

    client.write('position', {
      x: spawnPos.x,
      y: spawnPos.y,
      z: spawnPos.z,
      yaw: 0,
      pitch: 0,
      flags: 0x00
    })
  })

  await once(server, 'listening')
  return server
}

function add1x2Weight (entityIntersections, posX, posY, posZ, weight = 1) {
  entityIntersections[`${posX},${posY},${posZ}`] = entityIntersections[`${posX},${posY},${posZ}`] ?? 0
  entityIntersections[`${posX},${posY + 1},${posZ}`] = entityIntersections[`${posX},${posY + 1},${posZ}`] ?? 0

  entityIntersections[`${posX},${posY},${posZ}`] += weight
  entityIntersections[`${posX},${posY + 1},${posZ}`] += weight
}

describe('pathfinder Goals', function () {
  const mcData = require('minecraft-data')(Version)

  const targetBlock = new Vec3(12, 1, 8) // a gold block away from the spawn position
  const spawnPos = new Vec3(8.5, 1, 8.5) // Center of the chunk & center of the block

  /** @type { import('mineflayer').Bot & { pathfinder: import('mineflayer-pathfinder').Pathfinder }} */
  let bot
  /** @type { import('minecraft-protocol').Server } */
  let server

  before(async () => {
    const chunk = flatMap(Version)
    server = await newServer(server, chunk, spawnPos, Version, true)
    bot = mineflayer.createBot({
      username: 'player',
      version: Version,
      port: ServerPort
    })
    await once(bot, 'chunkColumnLoad')
  })
  after(() => {
    bot.end()
    bot = null
    server.close()
  })

  describe('Goals', () => {
    beforeEach(() => {
      bot.entity.position = spawnPos.clone()
    })

    it('GoalBlock', () => {
      const goal = new goals.GoalBlock(targetBlock.x, targetBlock.y, targetBlock.z)
      assert.ok(!goal.isEnd(bot.entity.position))
      bot.entity.position = targetBlock.clone()
      assert.ok(goal.isEnd(bot.entity.position))
    })

    it('GoalNear', () => {
      const goal = new goals.GoalNear(targetBlock.x, targetBlock.y, targetBlock.z, 1)
      assert.ok(!goal.isEnd(bot.entity.position))
      bot.entity.position = targetBlock.offset(1, 0, 0)
      assert.ok(goal.isEnd(bot.entity.position))
    })

    it('GoalXZ', () => {
      const goal = new goals.GoalXZ(targetBlock.x, targetBlock.z)
      assert.ok(!goal.isEnd(bot.entity.position))
      bot.entity.position = targetBlock.offset(0, 1, 0)
      assert.ok(goal.isEnd(bot.entity.position))
    })

    it('GoalNearXZ', () => {
      const goal = new goals.GoalNearXZ(targetBlock.x, targetBlock.z, 1)
      assert.ok(!goal.isEnd(bot.entity.position))
      bot.entity.position = targetBlock.offset(1, 0, 0)
      assert.ok(goal.isEnd(bot.entity.position))
    })

    it('GoalY', () => {
      const goal = new goals.GoalY(targetBlock.y + 1)
      assert.ok(!goal.isEnd(bot.entity.position))
      bot.entity.position = targetBlock.offset(0, 1, 0)
      assert.ok(goal.isEnd(bot.entity.position))
    })

    it('GoalGetToBlock', () => {
      const goal = new goals.GoalGetToBlock(targetBlock.x, targetBlock.y, targetBlock.z)
      assert.ok(!goal.isEnd(bot.entity.position))
      bot.entity.position = targetBlock.offset(1, 0, 0)
      assert.ok(goal.isEnd(bot.entity.position))
    })

    it('GoalCompositeAny', () => {
      const targetBlock2 = new Vec3(10, 1, 0)
      const goal1 = new goals.GoalBlock(targetBlock.x, targetBlock.y, targetBlock.z)
      const goal2 = new goals.GoalBlock(targetBlock2.x, targetBlock2.y, targetBlock2.z)
      const goalComposite = new goals.GoalCompositeAny()
      goalComposite.goals = [goal1, goal2]
      assert.ok(!goalComposite.isEnd(bot.entity.position))
      bot.entity.position = targetBlock.clone()
      assert.ok(goalComposite.isEnd(bot.entity.position)) // target block 1
      bot.entity.position = targetBlock2.clone()
      assert.ok(goalComposite.isEnd(bot.entity.position)) // target block 2
    })

    it('GoalCompositeAll', () => {
      const targetBlock = new Vec3(2, 1, 0)
      const block2 = new Vec3(3, 1, 0)
      const goal1 = new goals.GoalBlock(targetBlock.x, targetBlock.y, targetBlock.z)
      const goal2 = new goals.GoalNear(block2.x, block2.y, block2.z, 2)
      const goalComposite = new goals.GoalCompositeAll()
      goalComposite.goals = [goal1, goal2]
      assert.ok(!goalComposite.isEnd(bot.entity.position))
      bot.entity.position = targetBlock.offset(0, 0, 0)
      assert.ok(goalComposite.isEnd(bot.entity.position))
    })

    it('GoalInvert', () => {
      const goalBlock = new goals.GoalBlock(targetBlock.x, targetBlock.y, targetBlock.z)
      const goal = new goals.GoalInvert(goalBlock)
      bot.entity.position = targetBlock.clone()
      assert.ok(!goal.isEnd(bot.entity.position))
      bot.entity.position = new Vec3(0, 1, 0)
      assert.ok(goal.isEnd(bot.entity.position))
    })

    it('GoalPlaceBlock', () => {
      const placeTarget = targetBlock.offset(0, 1, 0)
      const goal = new goals.GoalPlaceBlock(placeTarget, bot.world, {})
      bot.entity.position = targetBlock.offset(-5, 0, 0) // to far away to reach
      assert.ok(!goal.isEnd(bot.entity.position.floored()))
      bot.entity.position = targetBlock.offset(-2, 0, 0)
      assert.ok(goal.isEnd(bot.entity.position.floored()))
    })

    it('GoalLookAtBlock', () => {
      const breakTarget = targetBlock.clone() // should be a gold block or any other block thats dig able
      const goal = new goals.GoalLookAtBlock(breakTarget, bot.world, { reach: 3 })
      assert.ok(!goal.isEnd(bot.entity.position.floored()))
      bot.entity.position = targetBlock.offset(-2, 0, 0) // should now be close enough
      assert.ok(goal.isEnd(bot.entity.position.floored()))
    })
  })

  describe('Goals with entity', () => {
    beforeEach(() => {
      bot.entity.position = spawnPos.clone()
    })
    before((done) => {
      const Entity = PEntity(Version)
      const chicken = new Entity(mcData.entitiesByName.chicken.id)
      const client = Object.values(server.clients)[0]
      client.write('spawn_entity', { // Might only work for 1.16
        entityId: chicken.id,
        objectUUID: uuidv4(),
        type: chicken.type,
        x: targetBlock.x,
        y: targetBlock.y + 1,
        z: targetBlock.z,
        pitch: 0,
        yaw: 0,
        objectData: 0,
        velocityX: 0,
        velocityY: 0,
        velocityZ: 0
      })
      setTimeout(done, 100)
    })

    it('GoalFollow', () => {
      const entity = bot.nearestEntity()
      const goal = new goals.GoalFollow(entity, 1)
      assert.ok(!goal.isEnd(bot.entity.position))
      bot.entity.position = targetBlock.clone()
      assert.ok(goal.isEnd(bot.entity.position))
    })
  })
})

describe('pathfinder events', function () {
  const mcData = require('minecraft-data')(Version)

  const targetBlock = new Vec3(12, 1, 8) // a gold block away from the spawn position
  const spawnPos = new Vec3(8.5, 1, 8.5) // Center of the chunk & center of the block

  /** @type { import('mineflayer').Bot & { pathfinder: import('mineflayer-pathfinder').Pathfinder }} */
  let bot
  /** @type { import('minecraft-protocol').Server } */
  let server

  before(async () => {
    const chunk = flatMap(Version)
    server = await newServer(server, chunk, spawnPos, Version, true)
    bot = mineflayer.createBot({
      username: 'player',
      version: Version,
      port: ServerPort
    })
    await once(bot, 'chunkColumnLoad')
    bot.loadPlugin(pathfinder)
    bot.pathfinder.setMovements(new Movements(bot, mcData))
  })
  after(() => server.close())

  describe('events', async function () {
    beforeEach(() => {
      bot.entity.position = spawnPos.clone()
    })
    afterEach((done) => {
      bot.pathfinder.setGoal(null)
      setTimeout(done)
      const listeners = ['goal_reached', 'goal_updated', 'path_update', 'path_stop']
      listeners.forEach(l => bot.removeAllListeners(l))
    })

    it('goal_reached', function (done) {
      this.timeout(3000)
      this.slow(1000)
      bot.once('goal_reached', () => done())
      bot.pathfinder.setGoal(new goals.GoalNear(targetBlock.x, targetBlock.y, targetBlock.z, 1))
    })

    it('goal_updated', function (done) {
      this.timeout(100)
      bot.once('goal_updated', () => done())
      bot.pathfinder.setGoal(new goals.GoalNear(targetBlock.x, targetBlock.y, targetBlock.z, 1))
    })

    it('path_update', function (done) {
      this.timeout(3000)
      this.slow(1000)
      bot.pathfinder.setGoal(new goals.GoalNear(targetBlock.x, targetBlock.y, targetBlock.z, 1))
      bot.once('path_update', () => done())
    })

    it('path_stop', function (done) {
      this.timeout(3000)
      this.slow(1000)
      bot.pathfinder.setGoal(new goals.GoalNear(targetBlock.x, targetBlock.y, targetBlock.z, 1))
      bot.once('path_stop', () => done())
      bot.pathfinder.stop()
    })
  })
})

describe('pathfinder util functions', function () {
  const mcData = require('minecraft-data')(Version)
  const Item = require('prismarine-item')(Version)

  const targetBlock = new Vec3(12, 1, 8) // a gold block away from the spawn position
  const spawnPos = new Vec3(8.5, 1, 8.5) // Center of the chunk & center of the block

  const itemsToGive = [new Item(mcData.itemsByName.diamond_pickaxe.id, 1), new Item(mcData.itemsByName.dirt.id, 64)]

  /** @type { import('mineflayer').Bot & { pathfinder: import('mineflayer-pathfinder').Pathfinder }} */
  let bot
  /** @type { import('minecraft-protocol').Server } */
  let server

  before(async () => {
    const chunk = flatMap(Version)
    server = await newServer(server, chunk, spawnPos, Version, true)
    bot = mineflayer.createBot({
      username: 'player',
      version: Version,
      port: ServerPort
    })
    await once(bot, 'chunkColumnLoad')
    itemsToGive.forEach(item => {
      const slot = bot.inventory.firstEmptyHotbarSlot()
      bot.inventory.slots[slot] = item
    })
    bot.loadPlugin(pathfinder)
    bot.pathfinder.setMovements(new Movements(bot, mcData))
  })
  after(() => server.close())

  describe('paththing', function () {
    this.afterEach((done) => {
      bot.pathfinder.setGoal(null)
      bot.entity.position = spawnPos.clone()
      bot.stopDigging()
      setTimeout(() => done())
    })

    it('Goto', async function () {
      this.timeout(3000)
      this.slow(1500)
      await bot.pathfinder.goto(new goals.GoalGetToBlock(targetBlock.x, targetBlock.y, targetBlock.z))
    })

    it('isMoving', function (done) {
      bot.pathfinder.setGoal(new goals.GoalGetToBlock(targetBlock.x, targetBlock.y, targetBlock.z))
      const foo = () => {
        if (bot.pathfinder.isMoving()) {
          bot.removeListener('physicTick', foo)
          done()
        }
      }
      bot.on('physicTick', foo)
    })

    // Note: Ordering seams to matter when running the isBuilding test. If run after isMining isBuilding does not seam to work.
    it('isBuilding', function (done) {
      this.timeout(5000)
      this.slow(1500)

      bot.pathfinder.setGoal(new goals.GoalBlock(targetBlock.x, targetBlock.y + 2, targetBlock.z))
      const foo = () => {
        if (bot.pathfinder.isBuilding()) {
          bot.removeListener('physicTick', foo)
          bot.stopDigging()
          done()
        }
      }
      bot.on('physicTick', foo)
    })

    it('isMining', function (done) {
      this.timeout(5000)
      this.slow(1500)

      bot.pathfinder.setGoal(new goals.GoalBlock(targetBlock.x, targetBlock.y, targetBlock.z))
      const foo = () => {
        if (bot.pathfinder.isMining()) {
          bot.removeListener('physicTick', foo)
          bot.stopDigging()
          done()
        }
      }
      bot.on('physicTick', foo)
    })
  })

  it('bestHarvestTool', function () {
    const block = bot.blockAt(targetBlock)
    const tool = bot.pathfinder.bestHarvestTool(block)
    assert.deepStrictEqual(tool, itemsToGive[0])
  })

  it('getPathTo', function () {
    const path = bot.pathfinder.getPathTo(bot.pathfinder.movements, new goals.GoalGetToBlock(targetBlock.x, targetBlock.y, targetBlock.z))
    // All depends on the actually path that gets generated. If target block is moved some were else these values have to change.
    assert.strictEqual(path.status, 'success')
    assert.ok(path.visitedNodes < 5, `Generated path visited nodes to high (${path.visitedNodes} < 5)`)
    assert.ok(path.generatedNodes < 30, `Generated path nodes to high (${path.generatedNodes} < 30)`)
    assert.ok(path.path.length === 3, `Generated path length wrong (${path.path.length} === 3)`)
    assert.ok(path.time < 50, `Generated path took too long (${path.time} < 50)`)
  })
})

describe('pathfinder Movement', function () {
  const mcData = require('minecraft-data')(Version)
  const Item = require('prismarine-item')(Version)
  const Block = require('prismarine-block')(Version)

  const targetBlock = new Vec3(12, 1, 8) // a gold block away from the spawn position
  const spawnPos = new Vec3(8.5, 1, 8.5) // Center of the chunk & center of the block

  /** @type { import('mineflayer').Bot & { pathfinder: import('mineflayer-pathfinder').Pathfinder }} */
  let bot
  /** @type { import('minecraft-protocol').Server } */
  let server
  /** @type { import('mineflayer-pathfinder').Movements } */
  let defaultMovement

  const itemsToGive = [new Item(mcData.itemsByName.diamond_pickaxe.id, 1), new Item(mcData.itemsByName.dirt.id, 64)]

  before(async () => {
    const chunk = flatMap(Version)
    server = await newServer(server, chunk, spawnPos, Version, true)
    bot = mineflayer.createBot({
      username: 'player',
      version: Version,
      port: ServerPort
    })
    await once(bot, 'chunkColumnLoad')
    itemsToGive.forEach(item => {
      const slot = bot.inventory.firstEmptyHotbarSlot()
      bot.inventory.slots[slot] = item
    })
    defaultMovement = new Movements(bot, mcData)
    bot.loadPlugin(pathfinder)
    bot.pathfinder.setMovements(defaultMovement)
  })
  after(() => server.close())

  it('countScaffoldingItems', function () {
    assert.strictEqual(defaultMovement.countScaffoldingItems(), 64)
  })

  it('getScaffoldingItem', function () {
    assert.strictEqual(defaultMovement.getScaffoldingItem(), itemsToGive[1])
  })

  it('getBlock', function () {
    assert.ok(defaultMovement.getBlock(targetBlock, 0, 0, 0).type === mcData.blocksByName.gold_block.id)
  })

  describe('safeToBreak world editing', function () {
    this.afterAll(async () => {
      defaultMovement.canDig = true
      await bot.world.setBlock(targetBlock.offset(1, 0, 0), new Block(mcData.blocksByName.air.id, 0))
    })

    it('safeToBreak', async function () {
      const block = bot.blockAt(targetBlock)
      assert.ok(defaultMovement.safeToBreak(block))
      defaultMovement.canDig = false
      assert.ok(!defaultMovement.safeToBreak(block))
      defaultMovement.canDig = true
      await bot.world.setBlock(targetBlock.offset(1, 0, 0), new Block(mcData.blocksByName.water.id, 0, 0))
      assert.ok(!defaultMovement.safeToBreak(block))
    })
  })

  it('safeOrBreak', function () {
    const block = defaultMovement.getBlock(targetBlock, 0, 0, 0)
    const toBreak = []
    const extraValue = defaultMovement.safeOrBreak(block, toBreak)
    assert.ok(extraValue < 100, `safeOrBreak to high for block (${extraValue} < 100)`)
    assert.ok(toBreak.length === 1, `safeOrBreak toBreak array wrong length ${toBreak.length} (${toBreak.length} === 1)`)
  })

  it('getMoveJumpUp', function () {
    const block = defaultMovement.getBlock(targetBlock, -1, 0, 0)
    const dir = new Vec3(1, 0, 0)
    const neighbors = []
    defaultMovement.getMoveJumpUp(block.position, dir, neighbors)
    assert.ok(neighbors.length === 1, `getMoveJumpUp neighbors not right length (${neighbors.length} === 1)`)
  })

  it('getMoveForward', function () {
    const dir = new Vec3(1, 0, 0)
    const neighbors = []
    defaultMovement.getMoveForward(targetBlock, dir, neighbors)
    assert.ok(neighbors.length === 1, `getMoveForward neighbors not right length (${neighbors.length} === 1)`)
  })

  it('getMoveDiagonal', function () {
    const dir = new Vec3(1, 0, 0)
    const neighbors = []
    defaultMovement.getMoveDiagonal(targetBlock, dir, neighbors)
    assert.ok(neighbors.length === 1, `getMoveDiagonal neighbors not right length (${neighbors.length} === 1)`)
  })

  it('getLandingBlock', function () {
    const node = targetBlock.offset(-1, 3, 0)
    const dir = new Vec3(1, 0, 0)
    const block = defaultMovement.getLandingBlock(node, dir)
    assert.ok(block != null, 'Landing block is null')
    if (!block) return
    assert.ok(block.type === mcData.blocksByName.air.id, `getLandingBlock not the right block (${block.name} === air)`)
    assert.ok(block.position.offset(0, -1, 0).distanceSquared(targetBlock) === 0, `getLandingBlock not landing (${block.position.offset(0, -1, 0).distanceSquared(targetBlock)}) on target block: ${defaultMovement.getBlock(block.position, 0, -1, 0).name}`)
  })

  it('getMoveDropDown', function () {
    const dir = new Vec3(1, 0, 0)
    const neighbors = []
    defaultMovement.getMoveDropDown(targetBlock.offset(-1, 4, 0), dir, neighbors)
    assert.ok(neighbors.length === 1, `getMoveDropDown neighbors not right length (${neighbors.length} === 1)`)
  })

  it('getMoveDown', function () {
    const neighbors = []
    defaultMovement.getMoveDown(targetBlock.offset(0, 4, 0), neighbors)
    assert.ok(neighbors.length === 1, `getMoveDown neighbors not right length (${neighbors.length} === 1)`)
  })

  it('getMoveUp', function () {
    const neighbors = []
    defaultMovement.getMoveUp(targetBlock.offset(0, 1, 0), neighbors)
    assert.ok(neighbors.length === 1, `getMoveUp neighbors not right length (${neighbors.length} === 1)`)
  })

  it('getNeighbors', function () {
    const neighbors = defaultMovement.getNeighbors(targetBlock.offset(0, 1, 0))
    assert.ok(neighbors.length > 0, 'getNeighbors length 0')
  })
})

describe('Parkour path test', function () {
  const mcData = require('minecraft-data')(Version)

  const spawnPos = new Vec3(8.5, 1, 8.5) // Center of the chunk & center of the block

  /** @type { import('mineflayer').Bot & { pathfinder: import('mineflayer-pathfinder').Pathfinder }} */
  let bot
  /** @type { import('minecraft-protocol').Server } */
  let server
  /** @type { import('mineflayer-pathfinder').Movements } */
  let defaultMovement

  const parkourSpawn1 = new Vec3(0.5, 3, 12.5)
  const parkourSpawn2 = new Vec3(5.5, 3, 12.5)

  before(async () => {
    this.timeout(5000)
    const chunk = await parkourMap(Version)
    server = await newServer(server, chunk, spawnPos, Version, true)
    bot = mineflayer.createBot({
      username: 'player',
      version: Version,
      port: ServerPort
    })
    await once(bot, 'chunkColumnLoad')
    defaultMovement = new Movements(bot, mcData)
    bot.loadPlugin(pathfinder)
    bot.pathfinder.setMovements(defaultMovement)
  })
  after(() => server.close())

  it('getMoveParkourForward-1', function () {
    const dirs = [new Vec3(0, 0, 1), new Vec3(0, 0, -1)]
    for (let i = 0; i < dirs.length; i++) {
      const dir = dirs[i] // only 2 dirs as the schematic parkour1.schem only has 2 other blocks to path to.
      const neighbors = []
      defaultMovement.getMoveParkourForward(parkourSpawn1, dir, neighbors)
      assert.ok(neighbors.length === 1, `getMoveParkourForward jump off gold block neighbors not right length (${neighbors.length} === 1)`)
    }
  })

  it('getMoveParkourForward-2', function () {
    const dirs = [new Vec3(1, 0, 0), new Vec3(0, 0, 1), new Vec3(-1, 0, 0), new Vec3(0, 0, -1)]
    for (let i = 0; i < dirs.length; i++) {
      const dir = dirs[i]
      const neighbors = []
      defaultMovement.getMoveParkourForward(parkourSpawn2, dir, neighbors)
      assert.ok(neighbors.length === 1, `getMoveParkourForward jump off gold block neighbors not right length (${neighbors.length} === 1)`)
    }
  })
})

describe('Physics test', function () {
  const mcData = require('minecraft-data')(Version)

  const spawnPos = new Vec3(8.5, 1, 8.5) // Center of the chunk & center of the block

  /** @type { import('mineflayer').Bot & { pathfinder: import('mineflayer-pathfinder').Pathfinder }} */
  let bot
  /** @type { import('minecraft-protocol').Server } */
  let server
  /** @type { import('mineflayer-pathfinder').Movements } */
  let defaultMovement

  const parkourSpawn1 = new Vec3(0.5, 3, 12.5)
  // const parkourSpawn2 = new Vec3(5.5, 3, 12.5)

  before(async () => {
    this.timeout(5000)
    const chunk = await parkourMap(Version)
    server = await newServer(server, chunk, spawnPos, Version, true)
    bot = mineflayer.createBot({
      username: 'player',
      version: Version,
      port: ServerPort
    })
    await once(bot, 'chunkColumnLoad')
    defaultMovement = new Movements(bot, mcData)
    bot.loadPlugin(pathfinder)
    bot.pathfinder.setMovements(defaultMovement)
  })
  after(() => server.close())

  it('simulateUntil', async function () {
    this.slow(1000)
    this.timeout(2000)
    const ticksToSimulate = 10
    const ticksPressForward = 5

    bot.entity.position = parkourSpawn1.clone()
    bot.entity.velocity = new Vec3(0, 0, 0)

    // Wait for the bot to be on the ground so bot.entity.onGround == true
    bot.clearControlStates()
    await once(bot, 'physicTick')
    await once(bot, 'physicTick')

    const physics = new Physics(bot)

    const simulatedSteps = []
    const realSteps = []

    const controller = (state, counter) => {
      state.control.forward = counter <= ticksPressForward
      state.control.jump = counter <= ticksPressForward
      simulatedSteps.push(state.pos.toString() + ' Input:' + String(counter <= ticksPressForward))
    }
    const state = physics.simulateUntil(() => false, controller, ticksToSimulate)
    simulatedSteps.push(state.pos.toString() + ' Input:false')

    // We have to be carful to not mess up the event scheduling. for await on(bot, 'physicTick') seams to work.
    // A for loop with just await once(bot, 'physicTick') does not always seam to work. What also works is attaching
    // a listener to bot with bot.on('physicTick', listener) but this is a lot nicer.
    let tick = 0
    for await (const _ of on(bot, 'physicTick')) { // eslint-disable-line no-unused-vars
      bot.setControlState('forward', tick <= ticksPressForward)
      bot.setControlState('jump', tick <= ticksPressForward)
      realSteps.push(bot.entity.position.toString() + ' Input:' + String(tick <= ticksPressForward))
      tick++
      if (tick > ticksToSimulate) break
    }

    bot.clearControlStates()
    // console.info(bot.entity.position.toString(), console.info(state.pos.toString()))
    assert.ok(bot.entity.position.distanceSquared(state.pos) < 0.01,
      `Simulated states don't match Bot: ${bot.entity.position.toString()} !== Simulation: ${state.pos.toString()}`
      // + '\nSimulated Steps:\n'
      // + simulatedSteps.join('\n') + '\n'
      // + 'Real steps:\n'
      // + realSteps.join('\n')
    )
  })

  // TODO: write test for simulateUntilNextTick
})

describe('pathfinder entity avoidance test', function () {
  const mcData = require('minecraft-data')(Version)

  const patherOptions = { resetEntityIntersects: false }
  const maxPathTime = 50

  const spawnPos = new Vec3(8.5, 1.0, 8.5) // Center of the chunk & center of the block

  /** @type { import('mineflayer').Bot & { pathfinder: import('mineflayer-pathfinder').Pathfinder }} */
  let bot
  /** @type { import('minecraft-protocol').Server } */
  let server
  /** @type { import('prismarine-chunk').Chunk } */
  let chunk

  before(async () => {
    chunk = await parkourMap(Version)
    server = await newServer(server, chunk, spawnPos, Version, true)
    bot = mineflayer.createBot({
      username: 'player',
      version: Version,
      port: ServerPort
    })
    await once(bot, 'chunkColumnLoad')

    bot.loadPlugin(pathfinder)
    bot.pathfinder.setMovements(new Movements(bot, mcData))
  })

  after(() => {
    bot.end()
    bot = null
    server.close()
  })

  /**
  * Ensure algorithm does not impede performance when handling a large number of entities
  */
  it('entityIndexPerformance', () => {
    const { performance } = require('perf_hooks')

    const targetBlock = new Vec3(11.5, 2.0, 10.5) // a gold block away from the spawn position
    const startPos = new Vec3(11.5, 2.0, 14.5) // Start point for test

    const goal = new goals.GoalGetToBlock(targetBlock.x, targetBlock.y, targetBlock.z)

    for (let i = 1; i <= 10000; i++) {
      const pos = (i % 2) === 0 ? new Vec3(10.5, 2.0, 12.5) : new Vec3(12.5, 2.0, 12.5)
      bot.entities[i] = { name: 'testEntity', position: pos, height: 2.0, width: 1.0 }
    }

    const beforeTime = performance.now()

    const generator = bot.pathfinder.getPathFromTo(bot.pathfinder.movements, startPos, goal)
    const { value: { result } } = generator.next()

    const timeElapsed = performance.now() - beforeTime

    bot.pathfinder.movements.clearCollisionIndex()
    for (let i = 1; i <= 10000; i++) {
      delete bot.entities[i]
    }

    assert.ok(timeElapsed < maxPathTime, `Generated path took too long (${result.time} < ${maxPathTime})`)
  })

  /**
   * Tests if bot will prefer a basic path with less entities
   * The test course is a 3x3x2 with a divider in the center
   * [O] = Open, [W] = Wall, [S] = Start, [E] = End
   *    W E W
   *  W O O O W
   *  W O W O W
   *  W O O O W
   *    W S W
   */
  describe('Weighted Path Avoidance', () => {
    const targetBlock = new Vec3(11.5, 2.0, 10.5) // a gold block away from the spawn position
    const startPos = new Vec3(11.5, 2.0, 14.5) // Start point for test
    const firstLeftNode = new Vec3(10.5, 2.0, 12.5)
    const firstRightNode = new Vec3(12.5, 2.0, 12.5)

    const goal = new goals.GoalGetToBlock(targetBlock.x, targetBlock.y, targetBlock.z)

    beforeEach((done) => {
      bot.pathfinder.movements.clearCollisionIndex()
      setTimeout(done, 100)
    })

    /**
     * By default, algorithm will favor the Left Path
     * [X] = Ent, [O] = Open, [W] = Wall
     *   O O O
     *   O W O
     *   O O O
     */
    it('defaultPath', () => {
      const generator = bot.pathfinder.getPathFromTo(bot.pathfinder.movements, startPos, goal, patherOptions)
      const { value: { result } } = generator.next()
      const path = result.path

      // Look at first and second nodes incase diagonal movements are used
      const leftBranch = (path[0].equals(firstLeftNode) || path[1].equals(firstLeftNode))
      const rightBranch = (path[0].equals(firstRightNode) || path[1].equals(firstRightNode))

      // All depends on the actually path that gets generated. If target block is moved some were else these values have to change.
      assert.strictEqual(result.status, 'success')
      assert.ok(result.time < maxPathTime, `Generated path took too long (${result.time} < ${maxPathTime})`)
      assert.ok(path.length === 3, `Generated path length wrong (${path.length} === 3)`)
      assert.ok(leftBranch === true, `Generated path did not follow Left Branch [Left Branch: ${leftBranch}, Right Branch: ${rightBranch}]`)
    })

    /**
     * Ensure path with weight is avoided
     * [X] = Ent, [O] = Open, [W] = Wall
     *   O O O
     *   O W X
     *   O O O
     */
    it('rightBranchObstructed', () => {
      add1x2Weight(bot.pathfinder.movements.entityIntersections, 12, 2, 12)

      const generator = bot.pathfinder.getPathFromTo(bot.pathfinder.movements, startPos, goal, patherOptions)
      const { value: { result } } = generator.next()
      const path = result.path

      // Look at first and second nodes incase diagonal movements are used
      const leftBranch = (path[0].equals(firstLeftNode) || path[1].equals(firstLeftNode))
      const rightBranch = (path[0].equals(firstRightNode) || path[1].equals(firstRightNode))

      // All depends on the actually path that gets generated. If target block is moved some were else these values have to change.
      assert.strictEqual(result.status, 'success')
      assert.ok(result.time < maxPathTime, `Generated path took too long (${result.time} < ${maxPathTime})`)
      assert.ok(path.length === 3, `Generated path length wrong (${path.length} === 3)`)
      assert.ok(leftBranch === true, `Generated path did not follow Left Branch [Left Branch: ${leftBranch}, Right Branch: ${rightBranch}]`)
    })

    /**
     * Ensure path with more weight is avoided
     * [X] = Ent, [O] = Open, [W] = Wall
     *   O O O
     *   X W X
     *   X O O
     */
    it('leftBranchMoreObstructed', () => {
      add1x2Weight(bot.pathfinder.movements.entityIntersections, 12, 2, 12)
      add1x2Weight(bot.pathfinder.movements.entityIntersections, 10, 2, 12)
      add1x2Weight(bot.pathfinder.movements.entityIntersections, 10, 2, 13)

      const generator = bot.pathfinder.getPathFromTo(bot.pathfinder.movements, startPos, goal, patherOptions)
      const { value: { result } } = generator.next()
      const path = result.path

      // Look at first and second nodes incase diagonal movements are used
      const leftBranch = (path[0].equals(firstLeftNode) || path[1].equals(firstLeftNode))
      const rightBranch = (path[0].equals(firstRightNode) || path[1].equals(firstRightNode))

      // All depends on the actually path that gets generated. If target block is moved some were else these values have to change.
      assert.strictEqual(result.status, 'success')
      assert.ok(result.time < maxPathTime, `Generated path took too long (${result.time} < ${maxPathTime})`)
      assert.ok(path.length === 3, `Generated path length wrong (${path.length} === 3)`)
      assert.ok(rightBranch === true, `Generated path did not follow Right Branch [Left Branch: ${leftBranch}, Right Branch: ${rightBranch}]`)
    })

    /**
     * Ensure blocks adjacent to diagonal nodes are detected
     * [X] = Ent, [O] = Open, [W] = Wall
     *   O O X
     *   X W O
     *   O O X
     */
    it('rightBranchDiagsClear', () => {
      add1x2Weight(bot.pathfinder.movements.entityIntersections, 12, 2, 13)
      add1x2Weight(bot.pathfinder.movements.entityIntersections, 12, 2, 11)
      add1x2Weight(bot.pathfinder.movements.entityIntersections, 10, 2, 12)

      const generator = bot.pathfinder.getPathFromTo(bot.pathfinder.movements, startPos, goal, patherOptions)
      const { value: { result } } = generator.next()
      const path = result.path

      // Look at first and second nodes incase diagonal movements are used
      const leftBranch = (path[0].equals(firstLeftNode) || path[1].equals(firstLeftNode))
      const rightBranch = (path[0].equals(firstRightNode) || path[1].equals(firstRightNode))

      // All depends on the actually path that gets generated. If target block is moved some were else these values have to change.
      assert.strictEqual(result.status, 'success')
      assert.ok(result.time < maxPathTime, `Generated path took too long (${result.time} < ${maxPathTime})`)
      assert.ok(path.length === 3, `Generated path length wrong (${path.length} === 3)`)
      assert.ok(leftBranch === true, `Generated path did not follow Left Branch [Left Branch: ${leftBranch}, Right Branch: ${rightBranch}]`)
    })

    /**
     * Ensure blocks adjacent to diagonal nodes are detected
     * [X] = Ent, [O] = Open, [W] = Wall
     *   X O O
     *   O W X
     *   X O O
     */
    it('leftBranchDiagsClear', () => {
      add1x2Weight(bot.pathfinder.movements.entityIntersections, 12, 2, 12)
      add1x2Weight(bot.pathfinder.movements.entityIntersections, 10, 2, 13)
      add1x2Weight(bot.pathfinder.movements.entityIntersections, 10, 2, 11)

      const generator = bot.pathfinder.getPathFromTo(bot.pathfinder.movements, startPos, goal, patherOptions)
      const { value: { result } } = generator.next()
      const path = result.path

      // Look at first and second nodes incase diagonal movements are used
      const leftBranch = (path[0].equals(firstLeftNode) || path[1].equals(firstLeftNode))
      const rightBranch = (path[0].equals(firstRightNode) || path[1].equals(firstRightNode))

      // All depends on the actually path that gets generated. If target block is moved some were else these values have to change.
      assert.strictEqual(result.status, 'success')
      assert.ok(result.time < maxPathTime, `Generated path took too long (${result.time} < ${maxPathTime})`)
      assert.ok(path.length === 3, `Generated path length wrong (${path.length} === 3)`)
      assert.ok(rightBranch === true, `Generated path did not follow Right Branch [Left Branch: ${leftBranch}, Right Branch: ${rightBranch}]`)
    })
  })

  /**
   * Tests if bot will try to path where they cannot build due to an entity and whether it will
   * try to break a block that would potentially cause an entity to fall.
   * The test course is a 2x2x4 pit where the start is at the bottom and the end is at the top
   * [O] = Open, [W] = Wall, [S] = Start, [E] = End
   *   W W W E
   *   W O O W
   *   W S O W
   *   W W W W
   */
  describe('Construction Path Avoidance', () => {
    const Item = require('prismarine-item')(Version)

    const scaffoldItemId = mcData.itemsByName.dirt.id
    const groundYPos = 2
    const lidYPos = 5
    const forwardPos = { x: 11, z: 6 }
    const leftPos = { x: 10, z: 6 }
    const rightPos = { x: 11, z: 7 }
    const backPos = { x: 10, z: 7 }
    const targetBlock = new Vec3(forwardPos.x + 1.5, lidYPos + 1.0, forwardPos.z - 0.5) // a gold block away from the spawn position. One block diagonal from forward
    const startPos = new Vec3(backPos.x + 0.5, groundYPos, backPos.z + 0.5) // Start point for test
    const firstLeftNode = new Vec3(leftPos.x + 0.5, groundYPos, leftPos.z + 0.5)
    const firstRightNode = new Vec3(rightPos.x + 0.5, groundYPos, rightPos.z + 0.5)
    const firstForwardNode = new Vec3(forwardPos.x + 0.5, groundYPos, forwardPos.z + 0.5)
    const firstBackNode = startPos.clone().plus(new Vec3(-0.5, 1, -0.5)) // Jump up isn't going to half block and targets one block higher

    const blockersToPlace = [forwardPos, leftPos, rightPos, backPos]
    const goal = new goals.GoalGetToBlock(targetBlock.x, targetBlock.y, targetBlock.z)

    /** @type { import('minecraft-protocol').Client } */
    let serverClient
    /** @type { number } */
    let hotbarSlot

    before(() => {
      serverClient = Object.values(server.clients)[0]
      hotbarSlot = bot.inventory.firstEmptyHotbarSlot()
    })

    beforeEach((done) => {
      bot.pathfinder.movements.clearCollisionIndex()
      bot.inventory.slots[hotbarSlot] = new Item(scaffoldItemId, 64)
      setTimeout(done, 100)
    })

    afterEach(async () => {
      blockersToPlace.forEach(hPos => {
        const blockPos = { x: hPos.x, y: lidYPos, z: hPos.z }
        serverClient.write('block_change', { location: blockPos, type: mcData.blocksByName.air.id })
        chunk.setBlockType(new Vec3(blockPos.x, blockPos.y, blockPos.z), mcData.blocksByName.air.id)
      })
      serverClient.write('map_chunk', generateChunkPacket(chunk))
      await once(bot, 'chunkColumnLoad')
    })

    /**
     * By default, algorithm will favor the Backward Path
     * [X] = Ent Below, [+] = Ent Above a Block, [O] = Open, [W] = Wall
     *   W W W W
     *   W O O W
     *   W O O W
     *   W W W W
     */
    it('defaultPath', () => {
      const generator = bot.pathfinder.getPathFromTo(bot.pathfinder.movements, startPos, goal, patherOptions)
      const { value: { result } } = generator.next()
      const path = result.path

      // Look at first and second nodes incase diagonal movements are used
      const leftBranch = (path[0].equals(firstLeftNode) || path[1].equals(firstLeftNode))
      const rightBranch = (path[0].equals(firstRightNode) || path[1].equals(firstRightNode))
      const forwardBranch = (path[0].equals(firstForwardNode) || path[1].equals(firstForwardNode))
      const backwardBranch = (path[0].equals(firstBackNode) || path[1].equals(firstBackNode))

      // All depends on the actually path that gets generated. If target block is moved some were else these values have to change.
      assert.strictEqual(result.status, 'success')
      assert.ok(result.time < maxPathTime, `Generated path took too long (${result.time} < ${maxPathTime})`)
      assert.ok(path.length === 6, `Generated path length wrong (${path.length} === 6)`)
      assert.ok(backwardBranch === true, `Generated path did not follow Backward Branch [Left Branch: ${leftBranch}, Right Branch: ${rightBranch}, Forward Branch: ${forwardBranch}, Backward Branch: ${backwardBranch}]`)
    })

    /**
     * Ensure bot finds a path when it cannot break left blocker with ent on top
     * [X] = Ent Below, [+] = Ent Above a Block, [O] = Open, [W] = Wall
     *   W W W W
     *   W O O W
     *   W + O W
     *   W W W W
     */
    it('backPathObstructed', async () => {
      const blockPos = { x: backPos.x, y: lidYPos, z: backPos.z }
      serverClient.write('block_change', { location: blockPos, type: mcData.blocksByName.dirt.id })
      chunk.setBlockType(new Vec3(blockPos.x, blockPos.y, blockPos.z), mcData.blocksByName.dirt.id)
      add1x2Weight(bot.pathfinder.movements.entityIntersections, blockPos.x, blockPos.y + 1, blockPos.z)

      serverClient.write('map_chunk', generateChunkPacket(chunk))
      await once(bot, 'chunkColumnLoad')

      const generator = bot.pathfinder.getPathFromTo(bot.pathfinder.movements, startPos, goal, patherOptions)
      const { value: { result } } = generator.next()
      const path = result.path

      // Look at first and second nodes incase diagonal movements are used
      const leftBranch = (path[0].equals(firstLeftNode) || path[1].equals(firstLeftNode))
      const rightBranch = (path[0].equals(firstRightNode) || path[1].equals(firstRightNode))
      const forwardBranch = (path[0].equals(firstForwardNode) || path[1].equals(firstForwardNode))
      const backwardBranch = (path[0].equals(firstBackNode) || path[1].equals(firstBackNode))

      // All depends on the actually path that gets generated. If target block is moved some were else these values have to change.
      assert.strictEqual(result.status, 'success')
      assert.ok(result.time < maxPathTime, `Generated path took too long (${result.time} < ${maxPathTime})`)
      assert.ok(path.length === 6, `Generated path length wrong (${path.length} === 6)`)
      assert.ok(rightBranch === true, `Generated path did not follow Right Branch [Left Branch: ${leftBranch}, Right Branch: ${rightBranch}, Forward Branch: ${forwardBranch}, Backward Branch: ${backwardBranch}]`)
    })

    /**
     * If there are blocks capping the pit with an entity above each block, ensure bot cannot path since
     * there are no blocks that can be broken without potentially dropping an entity. Bot is expected
     * to follow forward branch for as far as possible
     * [X] = Ent Below, [+] = Ent Above a Block, [O] = Open, [W] = Wall
     *   W W W W
     *   W + + W
     *   W + + W
     *   W W W W
     */
    it('noPathsBreakingObstructed', async () => {
      blockersToPlace.forEach(hPos => {
        const blockPos = { x: hPos.x, y: lidYPos, z: hPos.z }
        serverClient.write('block_change', { location: blockPos, type: mcData.blocksByName.dirt.id })
        chunk.setBlockType(new Vec3(blockPos.x, blockPos.y, blockPos.z), mcData.blocksByName.dirt.id)
        add1x2Weight(bot.pathfinder.movements.entityIntersections, blockPos.x, blockPos.y + 1, blockPos.z)
      })
      serverClient.write('map_chunk', generateChunkPacket(chunk))
      await once(bot, 'chunkColumnLoad')

      const generator = bot.pathfinder.getPathFromTo(bot.pathfinder.movements, startPos, goal, patherOptions)
      const { value: { result } } = generator.next()
      const path = result.path

      // Look at first and second nodes incase diagonal movements are used
      const leftBranch = (path[0].equals(firstLeftNode) || path[1].equals(firstLeftNode))
      const rightBranch = (path[0].equals(firstRightNode) || path[1].equals(firstRightNode))
      const forwardBranch = (path[0].equals(firstForwardNode) || path[1].equals(firstForwardNode))
      const backwardBranch = (path[0].equals(firstBackNode) || path[1].equals(firstBackNode))

      // All depends on the actually path that gets generated. If target block is moved some were else these values have to change.
      assert.strictEqual(result.status, 'noPath')
      assert.ok(result.time < maxPathTime, `Generated path took too long (${result.time} < ${maxPathTime})`)
      assert.ok(path.length === 2, `Generated path length wrong (${path.length} === 2)`)
      assert.ok(forwardBranch === true, `Generated path did not attempt to follow Forward Branch [Left Branch: ${leftBranch}, Right Branch: ${rightBranch}, Forward Branch: ${forwardBranch}, Backward Branch: ${backwardBranch}]`)
    })

    /**
     * If there are blocks capping the pit with an entity above each block, ensure bot can path
     * if allowed in the movements configuration
     * [X] = Ent Below, [+] = Ent Above a Block, [O] = Open, [W] = Wall
     *   W W W W
     *   W + + W
     *   W + + W
     *   W W W W
     */
    it('canPathWithBreakingObstructed', async () => {
      blockersToPlace.forEach(hPos => {
        const blockPos = { x: hPos.x, y: lidYPos, z: hPos.z }
        serverClient.write('block_change', { location: blockPos, type: mcData.blocksByName.dirt.id })
        chunk.setBlockType(new Vec3(blockPos.x, blockPos.y, blockPos.z), mcData.blocksByName.dirt.id)
        add1x2Weight(bot.pathfinder.movements.entityIntersections, blockPos.x, blockPos.y + 1, blockPos.z)
      })
      serverClient.write('map_chunk', generateChunkPacket(chunk))
      await once(bot, 'chunkColumnLoad')

      bot.pathfinder.movements.dontMineUnderFallingBlock = false
      const generator = bot.pathfinder.getPathFromTo(bot.pathfinder.movements, startPos, goal, patherOptions)
      const { value: { result } } = generator.next()
      const path = result.path
      bot.pathfinder.movements.dontMineUnderFallingBlock = true

      // Look at first and second nodes incase diagonal movements are used
      const leftBranch = (path[0].equals(firstLeftNode) || path[1].equals(firstLeftNode))
      const rightBranch = (path[0].equals(firstRightNode) || path[1].equals(firstRightNode))
      const forwardBranch = (path[0].equals(firstForwardNode) || path[1].equals(firstForwardNode))
      const backwardBranch = (path[0].equals(firstBackNode) || path[1].equals(firstBackNode))

      // All depends on the actually path that gets generated. If target block is moved some were else these values have to change.
      assert.strictEqual(result.status, 'success')
      assert.ok(result.time < maxPathTime, `Generated path took too long (${result.time} < ${maxPathTime})`)
      assert.ok(path.length === 6, `Generated path length wrong (${path.length} === 6)`)
      assert.ok(forwardBranch === true, `Generated path did not follow Forward Branch [Left Branch: ${leftBranch}, Right Branch: ${rightBranch}, Forward Branch: ${forwardBranch}, Backward Branch: ${backwardBranch}]`)
    })

    /**
     * If there are entities filling the entire build area, ensure bot cannot path since
     * entities will prevent any block placement. Bot is expected to follow forward branch
     * for as far as possible
     * [X] = Ent Below, [+] = Ent Above a Block, [O] = Open, [W] = Wall
     *   W W W W
     *   W X X W
     *   W X X W
     *   W W W W
     */
    it('noPathsBuildingObstructed', () => {
      blockersToPlace.forEach(hPos => {
        add1x2Weight(bot.pathfinder.movements.entityIntersections, hPos.x, groundYPos, hPos.z)
      })

      const generator = bot.pathfinder.getPathFromTo(bot.pathfinder.movements, startPos, goal, patherOptions)
      const { value: { result } } = generator.next()
      const path = result.path

      const leftBranch = path[0].equals(firstLeftNode)
      const rightBranch = path[0].equals(firstRightNode)
      const forwardBranch = path[0].equals(firstForwardNode)
      const backwardBranch = path[0].equals(firstBackNode)

      // All depends on the actually path that gets generated. If target block is moved some were else these values have to change.
      assert.strictEqual(result.status, 'noPath')
      assert.ok(result.time < maxPathTime, `Generated path took too long (${result.time} < ${maxPathTime})`)
      assert.ok(path.length === 1, `Generated path length wrong (${path.length} === 1)`)
      assert.ok(forwardBranch === true, `Generated path did not attempt to follow Forward Branch [Left Branch: ${leftBranch}, Right Branch: ${rightBranch}, Forward Branch: ${forwardBranch}, Backward Branch: ${backwardBranch}]`)
    })

    /**
     * If there are entities filling the entire build area except for one space, ensure bot finds a path
     * [X] = Ent Below, [+] = Ent Above a Block, [O] = Open, [W] = Wall
     *   W W W W
     *   W O X W
     *   W X X W
     *   W W W W
     */
    it('singlePathUnobstructed', () => {
      blockersToPlace.forEach(hPos => {
        if ((hPos.x !== leftPos.x) || (hPos.z !== leftPos.z)) {
          add1x2Weight(bot.pathfinder.movements.entityIntersections, hPos.x, groundYPos, hPos.z)
        }
      })

      const generator = bot.pathfinder.getPathFromTo(bot.pathfinder.movements, startPos, goal, patherOptions)
      const { value: { result } } = generator.next()
      const path = result.path

      // Look at first and second nodes incase diagonal movements are used
      const leftBranch = (path[0].equals(firstLeftNode) || path[1].equals(firstLeftNode))
      const rightBranch = (path[0].equals(firstRightNode) || path[1].equals(firstRightNode))
      const forwardBranch = (path[0].equals(firstForwardNode) || path[1].equals(firstForwardNode))
      const backwardBranch = (path[0].equals(firstBackNode) || path[1].equals(firstBackNode))

      // All depends on the actually path that gets generated. If target block is moved some were else these values have to change.
      assert.strictEqual(result.status, 'success')
      assert.ok(result.time < maxPathTime, `Generated path took too long (${result.time} < ${maxPathTime})`)
      assert.ok(path.length === 6, `Generated path length wrong (${path.length} === 6)`)
      assert.ok(leftBranch === true, `Generated path did not follow Left Branch [Left Branch: ${leftBranch}, Right Branch: ${rightBranch}, Forward Branch: ${forwardBranch}, Backward Branch: ${backwardBranch}]`)
    })
  })
})
