import {
  create,
  factory,
  all,
  MathJsFunctionName,
  fractionDependencies,
  addDependencies,
  divideDependencies,
  formatDependencies,
  MathNode,
} from 'mathjs'
import * as assert from 'assert'
import { expectTypeOf } from 'expect-type'

// This file serves a dual purpose:
// 1) examples of how to use math.js in TypeScript
// 2) tests for the TypeScript declarations provided by math.js

/*
Basic usage examples
*/
{
  const math = create(all)

  const m2by2 = [
    [-1, 2],
    [3, 1],
  ]
  const m2by3 = [
    [1, 2, 3],
    [4, 5, 6],
  ]

  // functions and constants
  math.round(math.e, 3)
  math.round(100.123, 3)
  math.atan2(3, -3) / math.pi
  math.log(10000, 10)
  math.sqrt(-4)

  math.pow(m2by2, 2)
  const angle = 0.2
  math.add(math.pow(math.sin(angle), 2), math.pow(math.cos(angle), 2))

  // std and variance check

  math.std(1, 2, 3)
  math.std([1, 2, 3])
  math.std([1, 2, 3], 'biased')
  math.std([1, 2, 3], 0, 'biased')
  math.std(m2by3, 1, 'unbiased')
  math.std(m2by3, 1, 'uncorrected')
  math.variance(1, 2, 3)
  math.variance([1, 2, 3])
  math.variance([1, 2, 3], 'biased')
  math.variance([1, 2, 3], 0, 'biased')
  math.variance(m2by3, 1, 'unbiased')
  math.variance(m2by3, 1, 'uncorrected')

  // std and variance on chain
  math.chain([1, 2, 3]).std('unbiased')
  math.chain(m2by3).std(0, 'biased').std(0, 'uncorrected')
  math.chain(m2by3).std(0, 'biased').std(0, 'uncorrected')
  math.chain([1, 2, 3]).std('unbiased')
  math.chain(m2by3).variance(0, 'biased')
  math.chain(m2by3).variance(1, 'uncorrected').variance('unbiased')

  // expressions
  math.evaluate('1.2 * (2 + 4.5)')

  // chained operations
  const a = math.chain(3).add(4).multiply(2).done()
  assert.strictEqual(a, 14)

  // mixed use of different data types in functions
  assert.deepStrictEqual(math.add(4, [5, 6]), [9, 10]) // number + Array
  assert.deepStrictEqual(
    math.multiply(math.unit('5 mm'), 3),
    math.unit('15 mm')
  ) // Unit * number
  assert.deepStrictEqual(math.subtract([2, 3, 4], 5), [-3, -2, -1]) // Array - number
  assert.deepStrictEqual(
    math.add(math.matrix([2, 3]), [4, 5]),
    math.matrix([6, 8])
  ) // Matrix + Array

  // narrowed type inference
  const _b: math.Matrix = math.add(math.matrix([2]), math.matrix([3]))
  const _c: math.Matrix = math.subtract(math.matrix([4]), math.matrix([5]))
}

/*
Bignumbers examples
*/
{
  // configure the default type of numbers as BigNumbers
  const math = create(all, {
    number: 'BigNumber',
    precision: 20,
  })

  {
    assert.deepStrictEqual(
      math.add(math.bignumber(0.1), math.bignumber(0.2)),
      math.bignumber(0.3)
    )
    assert.deepStrictEqual(
      math.divide(math.bignumber(0.3), math.bignumber(0.2)),
      math.bignumber(1.5)
    )
  }
}

/*
Chaining examples
*/
{
  const math = create(all, {})
  const a = math.chain(3).add(4).multiply(2).done()
  assert.strictEqual(a, 14)

  // Another example, calculate square(sin(pi / 4))
  const _b = math.chain(math.pi).divide(4).sin().square().done()

  // toString will return a string representation of the chain's value
  const chain = math.chain(2).divide(3)
  const str: string = chain.toString()
  assert.strictEqual(str, '0.6666666666666666')

  chain.valueOf()

  // the function subset can be used to get or replace sub matrices
  const array = [
    [1, 2],
    [3, 4],
  ]
  const v = math.chain(array).subset(math.index(1, 0)).done()
  assert.strictEqual(v, 3)

  const _m = math.chain(array).subset(math.index(0, 0), 8).multiply(3).done()

  // filtering
  assert.deepStrictEqual(
    math
      .chain([-1, 0, 1.1, 2, 3, 1000])
      .filter(math.isPositive)
      .filter(math.isInteger)
      .filter((n) => n !== 1000)
      .done(),
    [2, 3]
  )

  const r = math.chain(-0.483).round([0, 1, 2]).floor().add(0.52).fix(1).done()
  assert.deepStrictEqual(r, [0.5, -0.4, -0.4])

  expectTypeOf(math.chain('x + y').parse().resolve({ x: 1 }).done())
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    .toMatchTypeOf<any>()
  expectTypeOf(math.chain('x + y').parse().resolve().done())
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    .toMatchTypeOf<any>()
}

/*
Simplify examples
*/
{
  const math = create(all)

  math.simplify('2 * 1 * x ^ (2 - 1)')
  math.simplify('2 * 3 * x', { x: 4 })

  const f = math.parse('2 * 1 * x ^ (2 - 1)')
  math.simplify(f)

  math.simplify('0.4 * x', {}, { exactFractions: true })
  math.simplify('0.4 * x', {}, { exactFractions: false })
}

/*
Complex numbers examples
*/
{
  const math = create(all, {})
  const a = math.complex(2, 3)
  // create a complex number by providing a string with real and complex parts
  const b = math.complex('3 - 7i')

  // read the real and complex parts of the complex number
  {
    const _x: number = a.re
    const _y: number = a.im

    // adjust the complex value
    a.re = 5
  }

  // clone a complex value
  {
    const _clone = a.clone()
  }

  // perform operations with complex numbers
  {
    math.add(a, b)
    math.multiply(a, b)
    math.sin(a)
  }

  // create a complex number from polar coordinates
  {
    const p: math.PolarCoordinates = { r: math.sqrt(2), phi: math.pi / 4 }
    const _c: math.Complex = math.complex(p)
  }

  // get polar coordinates of a complex number
  {
    const _p: math.PolarCoordinates = math.complex(3, 4).toPolar()
  }
}

/*
Expressions examples
*/
{
  const math = create(all, {})
  // evaluate expressions
  {
    math.evaluate('sqrt(3^2 + 4^2)')
  }

  // evaluate multiple expressions at once
  {
    math.evaluate(['f = 3', 'g = 4', 'f * g'])
  }

  // get content of a parenthesis node
  {
    const node = math.parse('(1)')
    if (node.type !== 'ParenthesisNode') {
      throw Error(`expected ParenthesisNode, got ${node.type}`)
    }
    const _innerNode = node.content
  }

  // scope can contain both variables and functions
  {
    const scope = { hello: (name: string) => `hello, ${name}!` }
    assert.strictEqual(math.evaluate('hello("hero")', scope), 'hello, hero!')
  }

  // define a function as an expression
  {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const scope: any = {
      a: 3,
      b: 4,
    }
    const f = math.evaluate('f(x) = x ^ a', scope)
    f(2)
    scope.f(2)
  }

  {
    const node2 = math.parse('x^a')
    const _code2: math.EvalFunction = node2.compile()
    node2.toString()
  }

  // 3. using function math.compile
  // parse an expression
  {
    // provide a scope for the variable assignment
    const code2 = math.compile('a = a + 3')
    const scope = { a: 7 }
    code2.evaluate(scope)
  }
  // 4. using a parser
  const parser = math.parser()

  // get and set variables and functions
  {
    assert.strictEqual(parser.evaluate('x = 7 / 2'), 3.5)
    assert.strictEqual(parser.evaluate('x + 3'), 6.5)
    parser.evaluate('f(x, y) = x^y') // f(x, y)
    assert.strictEqual(parser.evaluate('f(2, 3)'), 8)

    const _x = parser.get('x')
    const f = parser.get('f')
    const _y = parser.getAll()
    const _g = f(3, 3)

    parser.set('h', 500)
    parser.set('hello', (name: string) => `hello, ${name}!`)
  }

  // clear defined functions and variables
  parser.clear()
}

/*
Fractions examples
*/
{
  // configure the default type of numbers as Fractions
  const math = create(all, {
    number: 'Fraction',
  })

  const x = math.fraction(0.125)
  const y = math.fraction('1/3')
  math.fraction(2, 3)

  math.add(x, y)
  math.divide(x, y)

  // output formatting
  const _a = math.fraction('2/3')
}

/*
Matrices examples
*/
{
  const math = create(all, {})

  // create matrices and arrays. a matrix is just a wrapper around an Array,
  // providing some handy utilities.
  const a: math.Matrix = math.matrix([1, 4, 9, 16, 25])
  const b: math.Matrix = math.matrix(math.ones([2, 3]))
  b.size()

  // the Array data of a Matrix can be retrieved using valueOf()
  const _array = a.valueOf()

  // Matrices can be cloned
  const _clone: math.Matrix = a.clone()

  // perform operations with matrices
  math.sqrt(a)
  math.factorial(a)

  // create and manipulate matrices. Arrays and Matrices can be used mixed.
  {
    const a = [
      [1, 2],
      [3, 4],
    ]
    const b: math.Matrix = math.matrix([
      [5, 6],
      [1, 1],
    ])

    b.subset(math.index(1, [0, 1]), [[7, 8]])
    const _c = math.multiply(a, b)
    const f: math.Matrix = math.matrix([1, 0])
    const _d: math.Matrix = f.subset(math.index(1))
  }

  // get a sub matrix
  {
    const a: math.Matrix = math.diag(math.range(1, 4))
    a.subset(math.index([1, 2], [1, 2]))
    const b: math.Matrix = math.range(1, 6)
    b.subset(math.index(math.range(1, 4)))
  }

  // resize a multi dimensional matrix
  {
    const a = math.matrix()
    a.resize([2, 2, 2], 0)
    a.size()
    a.resize([2, 2])
    a.size()
  }

  // can set a subset of a matrix to uninitialized
  {
    const m = math.matrix()
    m.subset(math.index(2), 6, math.uninitialized)
  }

  // create ranges
  {
    math.range(1, 6)
    math.range(0, 18, 3)
    math.range('2:-1:-3')
    math.factorial(math.range('1:6'))
  }

  // map matrix
  {
    assert.deepStrictEqual(
      math.map([1, 2, 3], function (value) {
        return value * value
      }),
      [1, 4, 9]
    )
  }

  // filter matrix
  {
    assert.deepStrictEqual(
      math.filter([6, -2, -1, 4, 3], function (x) {
        return x > 0
      }),
      [6, 4, 3]
    )
    assert.deepStrictEqual(
      math.filter(['23', 'foo', '100', '55', 'bar'], /[0-9]+/),
      ['23', '100', '55']
    )
  }

  // concat matrix
  {
    assert.deepStrictEqual(math.concat([[0, 1, 2]], [[1, 2, 3]]), [
      [0, 1, 2, 1, 2, 3],
    ])
    assert.deepStrictEqual(math.concat([[0, 1, 2]], [[1, 2, 3]], 0), [
      [0, 1, 2],
      [1, 2, 3],
    ])
  }

  // Matrix is available as a constructor for instanceof checks
  {
    assert.strictEqual(math.matrix([1, 2, 3]) instanceof math.Matrix, true)
  }
}

/*
Sparse matrices examples
*/
{
  const math = create(all, {})

  // create a sparse matrix
  const a = math.identity(1000, 1000, 'sparse')

  // do operations with a sparse matrix
  const b = math.multiply(a, a)
  const _c = math.multiply(b, math.complex(2, 2))
  const d = math.matrix([0, 1])
  const e = math.transpose(d)
  const _f = math.multiply(e, d)
}

/*
Units examples
*/
{
  const math = create(all, {})

  // units can be created by providing a value and unit name, or by providing
  // a string with a valued unit.
  const a = math.unit(45, 'cm') // 450 mm
  const b = math.unit('0.1m') // 100 mm
  const _c = math.unit(b)

  // creating units
  math.createUnit('foo')
  math.createUnit('furlong', '220 yards')
  math.createUnit('furlong', '220 yards', { override: true })
  math.createUnit('testunit', { definition: '0.555556 kelvin', offset: 459.67 })
  math.createUnit(
    'testunit',
    { definition: '0.555556 kelvin', offset: 459.67 },
    { override: true }
  )
  math.createUnit('knot', {
    definition: '0.514444 m/s',
    aliases: ['knots', 'kt', 'kts'],
  })
  math.createUnit(
    'knot',
    { definition: '0.514444 m/s', aliases: ['knots', 'kt', 'kts'] },
    { override: true }
  )
  math.createUnit(
    'knot',
    {
      definition: '0.514444 m/s',
      aliases: ['knots', 'kt', 'kts'],
      prefixes: 'long',
    },
    { override: true }
  )
  math.createUnit(
    {
      foo2: {
        prefixes: 'long',
      },
      bar: '40 foo',
      baz: {
        definition: '1 bar/hour',
        prefixes: 'long',
      },
    },
    {
      override: true,
    }
  )
  // use Unit as definition
  math.createUnit('c', { definition: b })
  math.createUnit('c', { definition: b }, { override: true })

  // units can be added, subtracted, and multiplied or divided by numbers and by other units
  math.add(a, b)
  math.multiply(b, 2)
  math.divide(math.unit('1 m'), math.unit('1 s'))
  math.pow(math.unit('12 in'), 3)

  // units can be converted to a specific type, or to a number
  b.to('cm')
  math.to(b, 'inch')
  b.toNumber('cm')
  math.number(b, 'cm')

  // the expression parser supports units too
  math.evaluate('2 inch to cm')

  // units can be converted to SI
  math.unit('1 inch').toSI()

  // units can be split into other units
  math.unit('1 m').splitUnit(['ft', 'in'])
}

/*
Expression tree examples
*/
{
  const math = create(all, {})

  // Filter an expression tree
  const node: math.MathNode = math.parse('x^2 + x/4 + 3*y')
  const filtered: math.MathNode[] = node.filter(
    (node: math.MathNode) => node.type === 'SymbolNode' && node.name === 'x'
  )

  const _arr: string[] = filtered.map((node: math.MathNode) => node.toString())

  // Traverse an expression tree
  const node1: math.MathNode = math.parse('3 * x + 2')
  node1.traverse(
    (node: math.MathNode, _path: string, _parent: math.MathNode) => {
      switch (node.type) {
        case 'OperatorNode':
          return node.type === 'OperatorNode'
        case 'ConstantNode':
          return node.type === 'ConstantNode'
        case 'SymbolNode':
          return node.type === 'SymbolNode'
        default:
          return
      }
    }
  )
}

/*
Function ceil examples
*/
{
  const math = create(all, {})

  // number input
  assert.strictEqual(math.ceil(3.2), 4)
  assert.strictEqual(math.ceil(-4.2), -4)

  // number input
  // roundoff result to 2 decimals
  assert.strictEqual(math.ceil(3.212, 2), 3.22)
  assert.deepStrictEqual(
    math.ceil(3.212, math.bignumber(2)),
    math.bignumber(3.22)
  )
  assert.strictEqual(math.ceil(-4.212, 2), -4.21)

  // bignumber input
  assert.deepStrictEqual(math.ceil(math.bignumber(3.212)), math.bignumber(4))
  assert.deepStrictEqual(
    math.ceil(math.bignumber(3.212), 2),
    math.bignumber(3.22)
  )
  assert.deepStrictEqual(
    math.ceil(math.bignumber(3.212), math.bignumber(2)),
    math.bignumber(3.22)
  )

  // fraction input
  assert.deepStrictEqual(math.ceil(math.fraction(44, 7)), math.fraction(7))
  assert.deepStrictEqual(math.ceil(math.fraction(-44, 7)), math.fraction(-6))
  assert.deepStrictEqual(
    math.ceil(math.fraction(44, 7), 2),
    math.fraction(629, 100)
  )
  assert.deepStrictEqual(
    math.ceil(math.fraction(44, 7), math.bignumber(2)),
    math.fraction(629, 100)
  )

  // Complex input
  const c = math.complex(3.24, -2.71)
  assert.deepStrictEqual(math.ceil(c), math.complex(4, -2))
  assert.deepStrictEqual(math.ceil(c, 1), math.complex(3.3, -2.7))
  assert.deepStrictEqual(
    math.ceil(c, math.bignumber(1)),
    math.complex(3.3, -2.7)
  )

  // array input
  assert.deepStrictEqual(math.ceil([3.2, 3.8, -4.7]), [4, 4, -4])
  assert.deepStrictEqual(math.ceil([3.21, 3.82, -4.71], 1), [3.3, 3.9, -4.7])
  assert.deepStrictEqual(
    math.ceil([3.21, 3.82, -4.71], math.bignumber(1)),
    math.bignumber([3.3, 3.9, -4.7])
  )

  // numeric input, array or matrix of decimals
  const numCeiled: math.MathArray = math.ceil(math.tau, [2, 3])
  assert.deepStrictEqual(numCeiled, [6.29, 6.284])
  const bigCeiled: math.Matrix = math.ceil(
    math.bignumber(6.28318),
    math.matrix([2, 3])
  )
  assert.deepStrictEqual(bigCeiled, math.matrix(math.bignumber([6.29, 6.284])))
  assert.deepStrictEqual(math.ceil(math.fraction(44, 7), [2, 3]), [
    math.fraction(629, 100),
    math.fraction(6286, 1000),
  ])

  // @ts-expect-error ... verify ceil(array, array) throws an error (for now)
  assert.throws(() => math.ceil([3.21, 3.82], [1, 2]), TypeError)
}

/*
Function fix examples
*/
{
  const math = create(all, {})

  // number input
  assert.strictEqual(math.fix(3.2), 3)
  assert.strictEqual(math.fix(-4.2), -4)

  // number input
  // roundoff result to 2 decimals
  assert.strictEqual(math.fix(3.212, 2), 3.21)
  assert.deepStrictEqual(
    math.fix(3.212, math.bignumber(2)),
    math.bignumber(3.21)
  )
  assert.strictEqual(math.fix(-4.212, 2), -4.21)

  // bignumber input
  assert.deepStrictEqual(math.fix(math.bignumber(3.212)), math.bignumber(3))
  assert.deepStrictEqual(
    math.fix(math.bignumber(3.212), 2),
    math.bignumber(3.21)
  )
  assert.deepStrictEqual(
    math.fix(math.bignumber(3.212), math.bignumber(2)),
    math.bignumber(3.21)
  )

  // fraction input
  assert.deepStrictEqual(math.fix(math.fraction(44, 7)), math.fraction(6))
  assert.deepStrictEqual(math.fix(math.fraction(-44, 7)), math.fraction(-6))
  assert.deepStrictEqual(
    math.fix(math.fraction(44, 7), 2),
    math.fraction(628, 100)
  )
  assert.deepStrictEqual(
    math.fix(math.fraction(44, 7), math.bignumber(2)),
    math.fraction(628, 100)
  )

  // Complex input
  const c = math.complex(3.24, -2.71)
  assert.deepStrictEqual(math.fix(c), math.complex(3, -2))
  assert.deepStrictEqual(math.fix(c, 1), math.complex(3.2, -2.7))
  assert.deepStrictEqual(
    math.fix(c, math.bignumber(1)),
    math.complex(3.2, -2.7)
  )

  // array input
  assert.deepStrictEqual(math.fix([3.2, 3.8, -4.7]), [3, 3, -4])
  assert.deepStrictEqual(math.fix([3.21, 3.82, -4.71], 1), [3.2, 3.8, -4.7])
  assert.deepStrictEqual(
    math.fix([3.21, 3.82, -4.71], math.bignumber(1)),
    math.bignumber([3.2, 3.8, -4.7])
  )

  // numeric input, array or matrix of decimals
  const numFixed: math.MathArray = math.fix(math.tau, [2, 3])
  assert.deepStrictEqual(numFixed, [6.28, 6.283])
  const bigFixed: math.Matrix = math.fix(
    math.bignumber(6.28318),
    math.matrix([2, 3])
  )
  assert.deepStrictEqual(bigFixed, math.matrix(math.bignumber([6.28, 6.283])))
  assert.deepStrictEqual(math.fix(math.fraction(44, 7), [2, 3]), [
    math.fraction(628, 100),
    math.fraction(6285, 1000),
  ])

  // @ts-expect-error ... verify fix(array, array) throws an error (for now)
  assert.throws(() => math.fix([3.21, 3.82], [1, 2]), TypeError)
}

/*
Function floor examples
*/
{
  const math = create(all, {})

  // number input
  assert.strictEqual(math.floor(3.2), 3)
  assert.strictEqual(math.floor(-4.2), -5)

  // number input
  // roundoff result to 2 decimals
  assert.strictEqual(math.floor(3.212, 2), 3.21)
  assert.deepStrictEqual(
    math.floor(3.212, math.bignumber(2)),
    math.bignumber(3.21)
  )
  assert.strictEqual(math.floor(-4.212, 2), -4.22)

  // bignumber input
  assert.deepStrictEqual(math.floor(math.bignumber(3.212)), math.bignumber(3))
  assert.deepStrictEqual(
    math.floor(math.bignumber(3.212), 2),
    math.bignumber(3.21)
  )
  assert.deepStrictEqual(
    math.floor(math.bignumber(3.212), math.bignumber(2)),
    math.bignumber(3.21)
  )

  // fraction input
  assert.deepStrictEqual(math.floor(math.fraction(44, 7)), math.fraction(6))
  assert.deepStrictEqual(math.floor(math.fraction(-44, 7)), math.fraction(-7))
  assert.deepStrictEqual(
    math.floor(math.fraction(44, 7), 2),
    math.fraction(628, 100)
  )
  assert.deepStrictEqual(
    math.floor(math.fraction(44, 7), math.bignumber(2)),
    math.fraction(628, 100)
  )

  // Complex input
  const c = math.complex(3.24, -2.71)
  assert.deepStrictEqual(math.floor(c), math.complex(3, -3))
  assert.deepStrictEqual(math.floor(c, 1), math.complex(3.2, -2.8))
  assert.deepStrictEqual(
    math.floor(c, math.bignumber(1)),
    math.complex(3.2, -2.8)
  )

  // array input
  assert.deepStrictEqual(math.floor([3.2, 3.8, -4.7]), [3, 3, -5])
  assert.deepStrictEqual(math.floor([3.21, 3.82, -4.71], 1), [3.2, 3.8, -4.8])
  assert.deepStrictEqual(
    math.floor([3.21, 3.82, -4.71], math.bignumber(1)),
    math.bignumber([3.2, 3.8, -4.8])
  )

  // numeric input, array or matrix of decimals
  const numFloored: math.MathArray = math.floor(math.tau, [2, 3])
  assert.deepStrictEqual(numFloored, [6.28, 6.283])
  const bigFloored: math.Matrix = math.floor(
    math.bignumber(6.28318),
    math.matrix([2, 3])
  )
  assert.deepStrictEqual(bigFloored, math.matrix(math.bignumber([6.28, 6.283])))
  assert.deepStrictEqual(math.floor(math.fraction(44, 7), [2, 3]), [
    math.fraction(628, 100),
    math.fraction(6285, 1000),
  ])

  // @ts-expect-error ... verify floor(array, array) throws an error (for now)
  assert.throws(() => math.floor([3.21, 3.82], [1, 2]), TypeError)
}

/*
Function round examples
*/
{
  const math = create(all, {})

  // number input
  assert.strictEqual(math.round(3.2), 3)
  assert.strictEqual(math.round(-4.2), -4)

  // number input
  // roundoff result to 2 decimals
  assert.strictEqual(math.round(3.212, 2), 3.21)
  assert.deepStrictEqual(
    math.round(3.212, math.bignumber(2)),
    math.bignumber(3.21)
  )
  assert.strictEqual(math.round(-4.212, 2), -4.21)

  // bignumber input
  assert.deepStrictEqual(math.round(math.bignumber(3.212)), math.bignumber(3))
  assert.deepStrictEqual(
    math.round(math.bignumber(3.212), 2),
    math.bignumber(3.21)
  )
  assert.deepStrictEqual(
    math.round(math.bignumber(3.212), math.bignumber(2)),
    math.bignumber(3.21)
  )

  // fraction input
  assert.deepStrictEqual(math.round(math.fraction(44, 7)), math.fraction(6))
  assert.deepStrictEqual(math.round(math.fraction(-44, 7)), math.fraction(-6))
  assert.deepStrictEqual(
    math.round(math.fraction(44, 7), 2),
    math.fraction(629, 100)
  )
  assert.deepStrictEqual(
    math.round(math.fraction(44, 7), math.bignumber(2)),
    math.fraction(629, 100)
  )

  // Complex input
  const c = math.complex(3.24, -2.71)
  assert.deepStrictEqual(math.round(c), math.complex(3, -3))
  assert.deepStrictEqual(math.round(c, 1), math.complex(3.2, -2.7))
  assert.deepStrictEqual(
    math.round(c, math.bignumber(1)),
    math.complex(3.2, -2.7)
  )

  // array input
  assert.deepStrictEqual(math.round([3.2, 3.8, -4.7]), [3, 4, -5])
  assert.deepStrictEqual(math.round([3.21, 3.82, -4.71], 1), [3.2, 3.8, -4.7])
  assert.deepStrictEqual(
    math.round([3.21, 3.82, -4.71], math.bignumber(1)),
    math.bignumber([3.2, 3.8, -4.7])
  )

  // numeric input, array or matrix of decimals
  const numRounded: math.MathArray = math.round(math.tau, [2, 3])
  assert.deepStrictEqual(numRounded, [6.28, 6.283])
  const bigRounded: math.Matrix = math.round(
    math.bignumber(6.28318),
    math.matrix([2, 3])
  )
  assert.deepStrictEqual(bigRounded, math.matrix(math.bignumber([6.28, 6.283])))
  assert.deepStrictEqual(math.round(math.fraction(44, 7), [2, 3]), [
    math.fraction(629, 100),
    math.fraction(6286, 1000),
  ])

  // @ts-expect-error ... verify round(array, array) throws an error (for now)
  assert.throws(() => math.round([3.21, 3.82], [1, 2]), TypeError)
}

/*
JSON serialization/deserialization
*/
{
  const math = create(all, {})

  const data = {
    bigNumber: math.bignumber('1.5'),
  }
  const stringified = JSON.stringify(data)
  const parsed = JSON.parse(stringified, math.reviver)
  assert.deepStrictEqual(parsed.bigNumber, math.bignumber('1.5'))
}

/*
Extend functionality with import
 */

declare module 'mathjs' {
  interface MathJsStatic {
    testFun(): number
    value: number
  }
}

{
  const math = create(all, {})
  const testFun = () => 5

  math.import(
    {
      testFun,
      value: 10,
    },
    {}
  )

  math.testFun()

  expectTypeOf(math.testFun()).toMatchTypeOf<number>()

  expectTypeOf(
    math.import({
      myvalue: 42,
      myFunc: (name: string) => `myFunc ${name}`,
    })
  ).toMatchTypeOf<void>()

  expectTypeOf(
    math.import(
      {
        myvalue: 42,
        myFunc: (name: string) => `myFunc ${name}`,
      },
      {
        override: true,
      }
    )
  ).toMatchTypeOf<void>()

  expectTypeOf(
    math.import(
      {
        myvalue2: 42,
      },
      {
        silent: true,
      }
    )
  ).toMatchTypeOf<void>()

  expectTypeOf(
    math.import(
      {
        myvalue3: 42,
      },
      {
        wrap: true,
      }
    )
  ).toMatchTypeOf<void>()

  expectTypeOf(
    math.import({
      myvalue4: 42,
    })
  ).toMatchTypeOf<void>()

  expectTypeOf(
    math.import([
      {
        myvalue5: 42,
      },
      {
        myFunc2: (name: string) => `myFunc2 ${name}`,
      },
    ])
  ).toMatchTypeOf<void>()
}

/*
Renamed functions from v5 => v6
 */
{
  const math = create(all, {})
  math.typeOf(1)
  math.variance([1, 2, 3, 4])
  math.evaluate('1 + 2')

  // chained operations
  math.chain(3).typeOf().done()
  math.chain([1, 2, 3]).variance().done()
  math.chain('1 + 2').evaluate().done()
}

/*
Factory Test
 */
{
  // create a factory function
  const name = 'negativeSquare'
  const dependencies: MathJsFunctionName[] = ['multiply', 'unaryMinus']
  const createNegativeSquare = factory(name, dependencies, (injected) => {
    const { multiply, unaryMinus } = injected
    return function negativeSquare(x: number): number {
      return unaryMinus(multiply(x, x))
    }
  })

  // create an instance of the function yourself:
  const multiply = (a: number, b: number) => a * b
  const unaryMinus = (a: number) => -a
  const negativeSquare = createNegativeSquare({ multiply, unaryMinus })
  negativeSquare(3)
}

/**
 * Dependency map typing test from mathjs official document:
 * https://mathjs.org/docs/custom_bundling.html#using-just-a-few-functions
 */
{
  const config = {
    // optionally, you can specify configuration
  }

  // Create just the functions we need
  const { fraction, add, divide, format } = create(
    {
      fractionDependencies,
      addDependencies,
      divideDependencies,
      formatDependencies,
    },
    config
  )

  // Use the created functions
  const a = fraction(1, 3)
  const b = fraction(3, 7)
  const c = add(a, b)
  const d = divide(a, b)
  assert.strictEqual(format(c), '16/21')
  assert.strictEqual(format(d), '7/9')
}

/**
 * Custom parsing functions
 * https://mathjs.org/docs/expressions/customization.html#customize-supported-characters
 */
{
  const math = create(all, {})
  const isAlphaOriginal = math.parse.isAlpha
  math.parse.isAlpha = (c, cPrev, cNext) => {
    return isAlphaOriginal(c, cPrev, cNext) || c === '\u260E'
  }

  // now we can use the \u260E (phone) character in expressions
  const result = math.evaluate('\u260Efoo', { '\u260Efoo': 42 })
  assert.strictEqual(result, 42)
}

/**
 * Util functions
 * https://mathjs.org/docs/reference/functions.html#utils-functions
 */
{
  const math = create(all, {})

  // hasNumericValue function
  assert.strictEqual(math.hasNumericValue(2), true)
  assert.strictEqual(math.hasNumericValue('2'), true)
  assert.strictEqual(math.isNumeric('2'), false)
  assert.strictEqual(math.hasNumericValue(0), true)
  assert.strictEqual(math.hasNumericValue(math.bignumber(500)), true)
  assert.deepStrictEqual(math.hasNumericValue([2.3, 'foo', false]), [
    true,
    false,
    true,
  ])
  assert.strictEqual(math.hasNumericValue(math.fraction(4)), true)
  assert.strictEqual(math.hasNumericValue(math.complex('2-4i')), false)
}

/**
 * src/util/is functions
 */
{
  const math = create(all, {})

  type IsFunc = (x: unknown) => boolean
  const isFuncs: IsFunc[] = [
    math.isNumber,
    math.isBigNumber,
    math.isComplex,
    math.isFraction,
    math.isUnit,
    math.isString,
    math.isArray,
    math.isMatrix,
    math.isCollection,
    math.isDenseMatrix,
    math.isSparseMatrix,
    math.isRange,
    math.isIndex,
    math.isBoolean,
    math.isResultSet,
    math.isHelp,
    math.isFunction,
    math.isDate,
    math.isRegExp,
    math.isObject,
    math.isNull,
    math.isUndefined,
    math.isAccessorNode,
    math.isArrayNode,
    math.isAssignmentNode,
    math.isBlockNode,
    math.isConditionalNode,
    math.isConstantNode,
    math.isFunctionAssignmentNode,
    math.isFunctionNode,
    math.isIndexNode,
    math.isNode,
    math.isObjectNode,
    math.isOperatorNode,
    math.isParenthesisNode,
    math.isRangeNode,
    math.isSymbolNode,
    math.isChain,
  ]

  isFuncs.forEach((f) => {
    const result = f(1)
    const isResultBoolean = result === true || result === false
    assert.ok(isResultBoolean)
  })

  // Check guards do type refinement

  let x: unknown

  if (math.isNumber(x)) {
    expectTypeOf(x).toMatchTypeOf<number>()
  }
  if (math.isBigNumber(x)) {
    expectTypeOf(x).toMatchTypeOf<math.BigNumber>()
  }
  if (math.isComplex(x)) {
    expectTypeOf(x).toMatchTypeOf<math.Complex>()
  }
  if (math.isFraction(x)) {
    expectTypeOf(x).toMatchTypeOf<math.Fraction>()
  }
  if (math.isUnit(x)) {
    expectTypeOf(x).toMatchTypeOf<math.Unit>()
  }
  if (math.isString(x)) {
    expectTypeOf(x).toMatchTypeOf<string>()
  }
  if (math.isArray(x)) {
    expectTypeOf(x).toMatchTypeOf<unknown[]>()
  }
  if (math.isMatrix(x)) {
    expectTypeOf(x).toMatchTypeOf<math.Matrix>()
  }
  if (math.isDenseMatrix(x)) {
    expectTypeOf(x).toMatchTypeOf<math.Matrix>()
  }
  if (math.isSparseMatrix(x)) {
    expectTypeOf(x).toMatchTypeOf<math.Matrix>()
  }
  if (math.isIndex(x)) {
    expectTypeOf(x).toMatchTypeOf<math.Index>()
  }
  if (math.isBoolean(x)) {
    expectTypeOf(x).toMatchTypeOf<boolean>()
  }
  if (math.isHelp(x)) {
    expectTypeOf(x).toMatchTypeOf<math.Help>()
  }
  if (math.isDate(x)) {
    expectTypeOf(x).toMatchTypeOf<Date>()
  }
  if (math.isRegExp(x)) {
    expectTypeOf(x).toMatchTypeOf<RegExp>()
  }
  if (math.isNull(x)) {
    expectTypeOf(x).toMatchTypeOf<null>()
  }
  if (math.isUndefined(x)) {
    expectTypeOf(x).toMatchTypeOf<undefined>()
  }

  if (math.isAccessorNode(x)) {
    expectTypeOf(x).toMatchTypeOf<math.AccessorNode>()
  }
  if (math.isArrayNode(x)) {
    expectTypeOf(x).toMatchTypeOf<math.ArrayNode>()
  }
  if (math.isAssignmentNode(x)) {
    expectTypeOf(x).toMatchTypeOf<math.AssignmentNode>()
  }
  if (math.isBlockNode(x)) {
    expectTypeOf(x).toMatchTypeOf<math.BlockNode>()
  }
  if (math.isConditionalNode(x)) {
    expectTypeOf(x).toMatchTypeOf<math.ConditionalNode>()
  }
  if (math.isConstantNode(x)) {
    expectTypeOf(x).toMatchTypeOf<math.ConstantNode>()
  }
  if (math.isFunctionAssignmentNode(x)) {
    expectTypeOf(x).toMatchTypeOf<math.FunctionAssignmentNode>()
  }
  if (math.isFunctionNode(x)) {
    expectTypeOf(x).toMatchTypeOf<math.FunctionNode>()
  }
  if (math.isIndexNode(x)) {
    expectTypeOf(x).toMatchTypeOf<math.IndexNode>()
  }
  if (math.isNode(x)) {
    expectTypeOf(x).toMatchTypeOf<math.MathNodeCommon>()
  }
  if (math.isNode(x)) {
    expectTypeOf(x).toMatchTypeOf<math.MathNodeCommon>()
  }
  if (math.isObjectNode(x)) {
    expectTypeOf(x).toMatchTypeOf<math.ObjectNode>()
  }
  if (math.isOperatorNode(x)) {
    expectTypeOf(x).toMatchTypeOf<math.OperatorNode>()
  }
  if (math.isParenthesisNode(x)) {
    expectTypeOf(x).toMatchTypeOf<math.ParenthesisNode>()
  }
  if (math.isRangeNode(x)) {
    expectTypeOf(x).toMatchTypeOf<math.RangeNode>()
  }
  if (math.isSymbolNode(x)) {
    expectTypeOf(x).toMatchTypeOf<math.SymbolNode>()
  }
  if (math.isChain(x)) {
    expectTypeOf(x).toMatchTypeOf<math.MathJsChain>()
  }
}

/*
Probability function examples
*/
{
  const math = create(all, {})

  expectTypeOf(math.lgamma(1.5)).toMatchTypeOf<number>()
  expectTypeOf(
    math.lgamma(math.complex(1.5, -1.5))
  ).toMatchTypeOf<math.Complex>()
}

/*
toTex examples
*/

{
  const math = create(all, {})

  expectTypeOf(math.parse('a/b').toTex()).toMatchTypeOf<string>()

  // TODO add proper types for toTex options
  expectTypeOf(
    math.parse('a/b').toTex({
      a: '123',
    })
  ).toMatchTypeOf<string>()
}

/*
Resolve examples
*/
{
  const math = create(all, {})

  expectTypeOf(math.resolve(math.parse('x + y'))).toMatchTypeOf<MathNode>()
  expectTypeOf(
    math.resolve(math.parse('x + y'), { x: 0 })
  ).toMatchTypeOf<MathNode>()
  expectTypeOf(math.resolve([math.parse('x + y')], { x: 0 })).toMatchTypeOf<
    MathNode[]
  >()
}
