/* global artifacts, web3, contract, assert */
require('chai')
  .use(require('bn-chai')(web3.utils.BN))
  .use(require('chai-as-promised'))
  .should()

const { takeSnapshot, revertSnapshot } = require('../lib/ganacheHelper')

const MerkleTreeWithHistory = artifacts.require('./MerkleTreeWithHistoryMock.sol')
const hasherContract = artifacts.require('./Hasher.sol')

const MerkleTree = require('../lib/MerkleTree')
const hasherImpl = require('../lib/MiMC')

const snarkjs = require('snarkjs')
const bigInt = snarkjs.bigInt

const { ETH_AMOUNT, MERKLE_TREE_HEIGHT } = process.env

// eslint-disable-next-line no-unused-vars
function BNArrayToStringArray(array) {
  const arrayToPrint = []
  array.forEach(item => {
    arrayToPrint.push(item.toString())
  })
  return arrayToPrint
}

function toFixedHex(number, length = 32) {
  let str = bigInt(number).toString(16)
  while (str.length < length * 2) str = '0' + str
  str = '0x' + str
  return str
}

contract('MerkleTreeWithHistory', accounts => {
  let merkleTreeWithHistory
  let hasherInstance
  let levels = MERKLE_TREE_HEIGHT || 16
  const sender = accounts[0]
  // eslint-disable-next-line no-unused-vars
  const value = ETH_AMOUNT || '1000000000000000000'
  let snapshotId
  let prefix = 'test'
  let tree
  let hasher

  before(async () => {
    tree = new MerkleTree(
      levels,
      null,
      prefix,
    )
    hasherInstance = await hasherContract.deployed()
    await MerkleTreeWithHistory.link(hasherContract, hasherInstance.address)
    merkleTreeWithHistory = await MerkleTreeWithHistory.new(levels)
    snapshotId = await takeSnapshot()
  })

  describe('#constructor', () => {
    it('should initialize', async () => {
      const zeroValue = await merkleTreeWithHistory.ZERO_VALUE()
      const firstSubtree = await merkleTreeWithHistory.filledSubtrees(0)
      firstSubtree.should.be.equal(toFixedHex(zeroValue))
      const firstZero = await merkleTreeWithHistory.zeros(0)
      firstZero.should.be.equal(toFixedHex(zeroValue))
    })
  })

  describe('merkleTreeLib', () => {
    it('index_to_key', () => {
      assert.equal(
        MerkleTree.index_to_key('test', 5, 20),
        'test_tree_5_20',
      )
    })

    it('tests insert', async () => {
      hasher = new hasherImpl()
      tree = new MerkleTree(
        2,
        null,
        prefix,
      )
      await tree.insert(toFixedHex('5'))
      let { root, path_elements } = await tree.path(0)
      const calculated_root = hasher.hash(null,
        hasher.hash(null, '5', path_elements[0]),
        path_elements[1]
      )
      // console.log(root)
      assert.equal(root, calculated_root)
    })
    it('creation odd elements count', async () => {
      const elements = [12, 13, 14, 15, 16, 17, 18, 19, 20]
      for(const [, el] of Object.entries(elements)) {
        await tree.insert(el)
      }

      const batchTree = new MerkleTree(
        levels,
        elements,
        prefix,
      )
      for(const [i] of Object.entries(elements)) {
        const pathViaConstructor = await batchTree.path(i)
        const pathViaUpdate = await tree.path(i)
        pathViaConstructor.should.be.deep.equal(pathViaUpdate)
      }
    })

    it('should find an element', async () => {
      const elements = [12, 13, 14, 15, 16, 17, 18, 19, 20]
      for(const [, el] of Object.entries(elements)) {
        await tree.insert(el)
      }
      let index = tree.getIndexByElement(13)
      index.should.be.equal(1)

      index = tree.getIndexByElement(19)
      index.should.be.equal(7)

      index = tree.getIndexByElement(12)
      index.should.be.equal(0)

      index = tree.getIndexByElement(20)
      index.should.be.equal(8)

      index = tree.getIndexByElement(42)
      index.should.be.equal(false)
    })

    it('creation even elements count', async () => {
      const elements = [12, 13, 14, 15, 16, 17]
      for(const [, el] of Object.entries(elements)) {
        await tree.insert(el)
      }

      const batchTree = new MerkleTree(
        levels,
        elements,
        prefix,
      )
      for(const [i] of Object.entries(elements)) {
        const pathViaConstructor = await batchTree.path(i)
        const pathViaUpdate = await tree.path(i)
        pathViaConstructor.should.be.deep.equal(pathViaUpdate)
      }
    })

    it.skip('creation using 30000 elements', () => {
      const elements = []
      for(let i = 1000; i < 31001; i++) {
        elements.push(i)
      }
      console.time('MerkleTree')
      tree = new MerkleTree(
        levels,
        elements,
        prefix,
      )
      console.timeEnd('MerkleTree')
      // 2,7 GHz Intel Core i7
      // 1000 : 1949.084ms
      // 10000: 19456.220ms
      // 30000: 63406.679ms
    })
  })

  describe('#insert', () => {
    it('should insert', async () => {
      let rootFromContract

      for (let i = 1; i < 11; i++) {
        await merkleTreeWithHistory.insert(toFixedHex(i), { from: sender })
        await tree.insert(i)
        let { root } = await tree.path(i - 1)
        rootFromContract = await merkleTreeWithHistory.getLastRoot()
        toFixedHex(root).should.be.equal(rootFromContract.toString())
      }
    })

    it('should reject if tree is full', async () => {
      const levels = 6
      const merkleTreeWithHistory = await MerkleTreeWithHistory.new(levels)

      for (let i = 0; i < 2**levels; i++) {
        await merkleTreeWithHistory.insert(toFixedHex(i+42)).should.be.fulfilled
      }

      let error = await merkleTreeWithHistory.insert(toFixedHex(1337)).should.be.rejected
      error.reason.should.be.equal('Merkle tree is full. No more leafs can be added')

      error = await merkleTreeWithHistory.insert(toFixedHex(1)).should.be.rejected
      error.reason.should.be.equal('Merkle tree is full. No more leafs can be added')
    })

    it.skip('hasher gas', async () => {
      const levels = 6
      const merkleTreeWithHistory = await MerkleTreeWithHistory.new(levels)
      const zeroValue = await merkleTreeWithHistory.zeroValue()

      const gas = await merkleTreeWithHistory.hashLeftRight.estimateGas(zeroValue, zeroValue)
      console.log('gas', gas - 21000)
    })
  })

  describe('#isKnownRoot', () => {
    it('should work', async () => {
      let path

      for (let i = 1; i < 5; i++) {
        await merkleTreeWithHistory.insert(toFixedHex(i), { from: sender }).should.be.fulfilled
        await tree.insert(i)
        path = await tree.path(i - 1)
        let isKnown = await merkleTreeWithHistory.isKnownRoot(toFixedHex(path.root))
        isKnown.should.be.equal(true)
      }

      await merkleTreeWithHistory.insert(toFixedHex(42), { from: sender }).should.be.fulfilled
      // check outdated root
      let isKnown = await merkleTreeWithHistory.isKnownRoot(toFixedHex(path.root))
      isKnown.should.be.equal(true)
    })

    it('should not return uninitialized roots', async () => {
      await merkleTreeWithHistory.insert(toFixedHex(42), { from: sender }).should.be.fulfilled
      let isKnown = await merkleTreeWithHistory.isKnownRoot(toFixedHex(0))
      isKnown.should.be.equal(false)
    })
  })


  afterEach(async () => {
    await revertSnapshot(snapshotId.result)
    // eslint-disable-next-line require-atomic-updates
    snapshotId = await takeSnapshot()
    hasher = new hasherImpl()
    tree = new MerkleTree(
      levels,
      null,
      prefix,
      null,
      hasher,
    )
  })
})
