// test BlockNode
const assert = require('assert')
const math = require('../../../src/main')
const Node = math.expression.node.Node
const ConstantNode = math.expression.node.ConstantNode
const SymbolNode = math.expression.node.SymbolNode
const RangeNode = math.expression.node.RangeNode
const AssignmentNode = math.expression.node.AssignmentNode
const OperatorNode = math.expression.node.OperatorNode
const BlockNode = math.expression.node.BlockNode
const ResultSet = math.type.ResultSet

describe('BlockNode', function () {
  it('should create a BlockNode', function () {
    const n = new BlockNode([])
    assert(n instanceof BlockNode)
    assert(n instanceof Node)
    assert.strictEqual(n.type, 'BlockNode')
  })

  it('should have isBlockNode', function () {
    const node = new BlockNode([])
    assert(node.isBlockNode)
  })

  it('should throw an error when calling without new operator', function () {
    assert.throws(function () { BlockNode() }, SyntaxError)
  })

  it('should throw an error when adding invalid blocks', function () {
    assert.throws(function () { console.log(new BlockNode()) }, /Array expected/)
    assert.throws(function () { console.log(new BlockNode([2])) }, /Property "node" must be a Node/)
    assert.throws(function () { console.log(new BlockNode([{ node: 2, visible: true }])) }, /Property "node" must be a Node/)
    assert.throws(function () { console.log(new BlockNode([{ node: new Node(), visible: 2 }])) }, /Property "visible" must be a boolean/)
  })

  it('should compile and evaluate a BlockNode', function () {
    const n = new BlockNode([
      {
        node: new ConstantNode(5),
        visible: true
      },
      {
        node: new AssignmentNode(new SymbolNode('foo'), new ConstantNode(3)),
        visible: false
      },
      {
        node: new SymbolNode('foo'),
        visible: true
      }
    ])

    let scope = {}
    assert.deepStrictEqual(n.compile().eval(scope), new ResultSet([5, 3]))
    assert.deepStrictEqual(scope, { foo: 3 })
  })

  it('expressions should be visible by default', function () {
    const n = new BlockNode([
      { node: new ConstantNode(5) }
    ])

    assert.deepStrictEqual(n.compile().eval(), new ResultSet([5]))
  })

  it('should filter a BlockNode', function () {
    const a = new ConstantNode(5)
    const b2 = new ConstantNode(3)
    const foo = new SymbolNode('foo')
    const b = new AssignmentNode(foo, b2)
    const c = new SymbolNode('foo')
    const d = new BlockNode([
      { node: a, visible: true },
      { node: b, visible: false },
      { node: c, visible: true }
    ])

    assert.deepStrictEqual(d.filter(function (node) { return node instanceof BlockNode }), [d])
    assert.deepStrictEqual(d.filter(function (node) { return node instanceof SymbolNode }), [foo, c])
    assert.deepStrictEqual(d.filter(function (node) { return node instanceof RangeNode }), [])
    assert.deepStrictEqual(d.filter(function (node) { return node instanceof ConstantNode }), [a, b2])
    assert.deepStrictEqual(d.filter(function (node) { return node instanceof ConstantNode && node.value === 3 }), [b2])
  })

  it('should run forEach on a BlockNode', function () {
    // [x, 2]
    const x = new SymbolNode('x')
    const two = new ConstantNode(2)
    const c = new OperatorNode('+', 'add', [two, x])
    const a = new BlockNode([
      { node: x },
      { node: c }
    ])

    const nodes = []
    const paths = []
    a.forEach(function (node, path, parent) {
      nodes.push(node)
      paths.push(path)
      assert.strictEqual(parent, a)
    })

    assert.strictEqual(nodes.length, 2)
    assert.strictEqual(nodes[0], x)
    assert.strictEqual(nodes[1], c)
    assert.deepStrictEqual(paths, ['blocks[0].node', 'blocks[1].node'])
  })

  it('should map a BlockNode', function () {
    // [x, 2]
    const x = new SymbolNode('x')
    const two = new ConstantNode(2)
    const c = new OperatorNode('+', 'add', [two, x])
    const a = new BlockNode([
      { node: x },
      { node: c }
    ])

    const nodes = []
    const paths = []
    const d = new ConstantNode(3)
    const e = a.map(function (node, path, parent) {
      nodes.push(node)
      paths.push(path)
      assert.strictEqual(parent, a)
      return node instanceof SymbolNode && node.name === 'x' ? d : node
    })

    assert.strictEqual(nodes.length, 2)
    assert.strictEqual(nodes[0], x)
    assert.strictEqual(nodes[1], c)
    assert.deepStrictEqual(paths, ['blocks[0].node', 'blocks[1].node'])

    assert.notStrictEqual(e, a)
    assert.strictEqual(e.blocks[0].node, d)
    assert.strictEqual(e.blocks[1].node, c)

    // should not touch nested nodes
    assert.strictEqual(e.blocks[1].node.args[0], two)
    assert.strictEqual(e.blocks[1].node.args[1], x)
  })

  it('should throw an error when the map callback does not return a node', function () {
    const x = new SymbolNode('x')
    const two = new ConstantNode(2)
    const c = new OperatorNode('+', 'add', [two, x])
    const a = new BlockNode([
      { node: x },
      { node: c }
    ])

    assert.throws(function () {
      a.map(function () {})
    }, /Callback function must return a Node/)
  })

  it('should transform a BlockNodes parameters', function () {
    // [x, 2]
    const b = new SymbolNode('x')
    const c = new ConstantNode(2)
    const a = new BlockNode([
      { node: b },
      { node: c }
    ])

    const d = new ConstantNode(3)
    const e = a.transform(function (node) {
      return node instanceof SymbolNode && node.name === 'x' ? d : node
    })

    assert.notStrictEqual(e, a)
    assert.deepStrictEqual(e.blocks[0].node, d)
    assert.deepStrictEqual(e.blocks[1].node, c)
  })

  it('should transform a BlockNode itself', function () {
    // [x, 2]
    const a = new BlockNode([])

    const d = new ConstantNode(3)
    const e = a.transform(function (node) {
      return node instanceof BlockNode ? d : node
    })

    assert.notStrictEqual(e, a)
    assert.deepStrictEqual(e, d)
  })

  it('should traverse a BlockNode', function () {
    const a = new ConstantNode(1)
    const b = new ConstantNode(2)
    const c = new BlockNode([
      { node: a, visible: true },
      { node: b, visible: true }
    ])

    let count = 0
    c.traverse(function (node, index, parent) {
      count++

      switch (count) {
        case 1:
          assert.strictEqual(node, c)
          assert.strictEqual(index, null)
          assert.strictEqual(parent, null)
          break

        case 2:
          assert.strictEqual(node, a)
          assert.strictEqual(index, 'blocks[0].node')
          assert.strictEqual(parent, c)
          break

        case 3:
          assert.strictEqual(node, b)
          assert.strictEqual(index, 'blocks[1].node')
          assert.strictEqual(parent, c)
          break
      }
    })

    assert.strictEqual(count, 3)
  })

  it('should clone a BlockNode', function () {
    // [x, 2]
    const b = new SymbolNode('x')
    const c = new ConstantNode(2)
    const a = new BlockNode([
      { node: b },
      { node: c }
    ])

    const d = a.clone()
    assert(d instanceof BlockNode)
    assert.deepStrictEqual(a, d)
    assert.notStrictEqual(a, d)
    assert.notStrictEqual(a.blocks, d.blocks)
    assert.notStrictEqual(a.blocks[0], d.blocks[0])
    assert.notStrictEqual(a.blocks[1], d.blocks[1])
    assert.strictEqual(a.blocks[0].node, d.blocks[0].node)
    assert.strictEqual(a.blocks[1].node, d.blocks[1].node)
  })

  it('test equality another Node', function () {
    const a = new BlockNode([ { node: new SymbolNode('x') }, { node: new ConstantNode(2) } ])
    const b = new BlockNode([ { node: new SymbolNode('x') }, { node: new ConstantNode(2) } ])
    const c = new BlockNode([ { node: new SymbolNode('x') }, { node: new ConstantNode(4) } ])
    const d = new BlockNode([ { node: new SymbolNode('x') }, { node: new ConstantNode(2), visible: false } ])
    const e = new BlockNode([ { node: new SymbolNode('x') }, { node: new ConstantNode(2) }, { node: new ConstantNode(5) } ])

    assert.strictEqual(a.equals(null), false)
    assert.strictEqual(a.equals(undefined), false)
    assert.strictEqual(a.equals(b), true)
    assert.strictEqual(a.equals(c), false)
    assert.strictEqual(a.equals(d), false)
    assert.strictEqual(a.equals(e), false)
  })

  it('should stringify a BlockNode', function () {
    const n = new BlockNode([
      { node: new ConstantNode(5), visible: true },
      { node: new AssignmentNode(new SymbolNode('foo'), new ConstantNode(3)), visible: false },
      { node: new SymbolNode('foo'), visible: true }
    ])

    assert.strictEqual(n.toString(), '5\nfoo = 3;\nfoo')
  })

  it('should stringify a BlockNode with custom toString', function () {
    // Also checks if the custom functions get passed on to the children
    const customFunction = function (node, options) {
      if (node.type === 'BlockNode') {
        let string = ''
        node.blocks.forEach(function (block) {
          string += block.node.toString(options) + '; '
        })

        return string
      } else if (node.type === 'ConstantNode') {
        return 'const(' + node.value + ', ' + math.typeof(node.value) + ')'
      }
    }

    const a = new ConstantNode(1)
    const b = new ConstantNode(2)

    const n = new BlockNode([{ node: a }, { node: b }])

    assert.strictEqual(n.toString({ handler: customFunction }), 'const(1, number); const(2, number); ')
  })

  it('toJSON and fromJSON', function () {
    const b = new ConstantNode(1)
    const c = new ConstantNode(2)

    const bBlock = { node: b, visible: false }
    const cBlock = { node: c, visible: true }

    const node = new BlockNode([bBlock, cBlock])

    const json = node.toJSON()

    assert.deepStrictEqual(json, {
      mathjs: 'BlockNode',
      blocks: [ bBlock, cBlock ]
    })

    const parsed = BlockNode.fromJSON(json)
    assert.deepStrictEqual(parsed, node)
  })

  it('should LaTeX a BlockNode', function () {
    const n = new BlockNode([
      { node: new ConstantNode(5), visible: true },
      { node: new AssignmentNode(new SymbolNode('foo'), new ConstantNode(3)), visible: false },
      { node: new SymbolNode('foo'), visible: true }
    ])

    assert.strictEqual(n.toTex(), '5\\;\\;\n foo:=3;\\;\\;\n foo')
  })

  it('should LaTeX a BlockNode with custom toTex', function () {
    // Also checks if the custom functions get passed on to the children
    const customFunction = function (node, options) {
      if (node.type === 'BlockNode') {
        let latex = ''
        node.blocks.forEach(function (block) {
          latex += block.node.toTex(options) + '; '
        })

        return latex
      } else if (node.type === 'ConstantNode') {
        return 'const\\left(' + node.value + ', ' + math.typeof(node.value) + '\\right)'
      }
    }

    const a = new ConstantNode(1)
    const b = new ConstantNode(2)

    const n = new BlockNode([{ node: a }, { node: b }])

    assert.strictEqual(n.toTex({ handler: customFunction }), 'const\\left(1, number\\right); const\\left(2, number\\right); ')
  })
})
