import { blockchainTests, expect, hexRandom, Numberish } from '@0x/contracts-test-utils';
import { BigNumber, FixedMathRevertErrors } from '@0x/utils';
import { Decimal } from 'decimal.js';
import * as _ from 'lodash';

import { assertRoughlyEquals, fromFixed, toDecimal, toFixed } from '../utils/number_utils';

import { artifacts } from '../artifacts';
import { TestLibFixedMathContract } from '../wrappers';

blockchainTests('LibFixedMath unit tests', env => {
    let testContract: TestLibFixedMathContract;

    before(async () => {
        testContract = await TestLibFixedMathContract.deployFrom0xArtifactAsync(
            artifacts.TestLibFixedMath,
            env.provider,
            env.txDefaults,
            artifacts,
        );
    });

    const BITS_OF_PRECISION = 127;
    const FIXED_POINT_DIVISOR = new BigNumber(2).pow(BITS_OF_PRECISION);
    const FIXED_1 = FIXED_POINT_DIVISOR;
    const MAX_FIXED_VALUE = new BigNumber(2).pow(255).minus(1);
    const MIN_FIXED_VALUE = new BigNumber(2).pow(255).times(-1);
    const MIN_EXP_NUMBER = new BigNumber('-63.875');
    const MAX_EXP_NUMBER = new BigNumber(0);
    // e ^ MIN_EXP_NUMBER
    const MIN_LN_NUMBER = new BigNumber(new Decimal(MIN_EXP_NUMBER.toFixed(128)).exp().toFixed(128));
    const FUZZ_COUNT = 1024;

    function assertFixedEquals(actualFixed: Numberish, expected: Numberish): void {
        expect(fromFixed(actualFixed)).to.bignumber.eq(fromFixed(toFixed(expected)));
    }

    function assertFixedRoughlyEquals(actualFixed: Numberish, expected: Numberish, precision: number = 18): void {
        assertRoughlyEquals(fromFixed(actualFixed), expected, precision);
    }

    describe('one()', () => {
        it('equals 1', async () => {
            const r = await testContract.one().callAsync();
            assertFixedEquals(r, 1);
        });
    });

    describe('abs()', () => {
        it('abs(n) == n', async () => {
            const n = 1337.5912;
            const r = await testContract.abs(toFixed(n)).callAsync();
            assertFixedEquals(r, n);
        });

        it('abs(-n) == n', async () => {
            const n = -1337.5912;
            const r = await testContract.abs(toFixed(n)).callAsync();
            assertFixedEquals(r, -n);
        });

        it('abs(0) == 0', async () => {
            const n = 0;
            const r = await testContract.abs(toFixed(n)).callAsync();
            expect(r).to.bignumber.eq(0);
        });

        it('abs(MAX_FIXED) == MAX_FIXED', async () => {
            const n = MAX_FIXED_VALUE;
            const r = await testContract.abs(n).callAsync();
            expect(r).to.bignumber.eq(n);
        });

        it('abs(MIN_FIXED) throws', async () => {
            const n = MIN_FIXED_VALUE;
            const expectedError = new FixedMathRevertErrors.SignedValueError(
                FixedMathRevertErrors.ValueErrorCodes.TooSmall,
                n,
            );
            const tx = testContract.abs(n).callAsync();
            return expect(tx).to.revertWith(expectedError);
        });

        it('abs(int(-1)) == int(1)', async () => {
            const n = -1;
            const r = await testContract.abs(new BigNumber(n)).callAsync();
            expect(r).to.bignumber.eq(1);
        });

        it('abs(int(1)) == int(1)', async () => {
            const n = 1;
            const r = await testContract.abs(new BigNumber(n)).callAsync();
            expect(r).to.bignumber.eq(1);
        });
    });

    describe('invert()', () => {
        it('invert(1) == 1', async () => {
            const n = 1;
            const r = await testContract.invert(toFixed(n)).callAsync();
            assertFixedEquals(r, n);
        });

        it('invert(n) == 1 / n', async () => {
            const n = 1337.5912;
            const r = await testContract.invert(toFixed(n)).callAsync();
            assertFixedRoughlyEquals(r, 1 / n);
        });

        it('invert(-n) == -1 / n', async () => {
            const n = -1337.5912;
            const r = await testContract.invert(toFixed(n)).callAsync();
            assertFixedRoughlyEquals(r, 1 / n);
        });

        it('invert(0) throws', async () => {
            const expectedError = new FixedMathRevertErrors.BinOpError(
                FixedMathRevertErrors.BinOpErrorCodes.DivisionByZero,
            );
            const tx = testContract.invert(toFixed(0)).callAsync();
            return expect(tx).to.revertWith(expectedError);
        });
    });

    describe('mulDiv()', () => {
        it('mulDiv(0, 0, 1) == 0', async () => {
            const [a, n, d] = [0, 0, 1];
            const r = await testContract.mulDiv(toFixed(a), new BigNumber(n), new BigNumber(d)).callAsync();
            assertFixedEquals(r, 0);
        });

        it('mulDiv(0, x, y) == 0', async () => {
            const [a, n, d] = [0, 13, 300];
            const r = await testContract.mulDiv(toFixed(a), new BigNumber(n), new BigNumber(d)).callAsync();
            assertFixedEquals(r, 0);
        });

        it('mulDiv(x, y, y) == x', async () => {
            const [a, n, d] = [1.2345, 149, 149];
            const r = await testContract.mulDiv(toFixed(a), new BigNumber(n), new BigNumber(d)).callAsync();
            assertFixedEquals(r, a);
        });

        it('mulDiv(x, -y, y) == -x', async () => {
            const [a, n, d] = [1.2345, -149, 149];
            const r = await testContract.mulDiv(toFixed(a), new BigNumber(n), new BigNumber(d)).callAsync();
            assertFixedEquals(r, -a);
        });

        it('mulDiv(-x, -y, y) == x', async () => {
            const [a, n, d] = [-1.2345, -149, 149];
            const r = await testContract.mulDiv(toFixed(a), new BigNumber(n), new BigNumber(d)).callAsync();
            assertFixedEquals(r, -a);
        });

        it('mulDiv(x, y, 0) throws', async () => {
            const [a, n, d] = [1.2345, 149, 0];
            const expectedError = new FixedMathRevertErrors.BinOpError(
                FixedMathRevertErrors.BinOpErrorCodes.DivisionByZero,
            );
            const tx = testContract.mulDiv(toFixed(a), new BigNumber(n), new BigNumber(d)).callAsync();
            return expect(tx).to.revertWith(expectedError);
        });

        it('mulDiv(int(-1), int(1), int(-1)) == int(1)', async () => {
            const [a, n, d] = [-1, 1, -1];
            const r = await testContract.mulDiv(new BigNumber(a), new BigNumber(n), new BigNumber(d)).callAsync();
            assertFixedEquals(r, fromFixed(1));
        });

        it('mulDiv(int(1), int(-1), int(-1)) == int(1)', async () => {
            const [a, n, d] = [1, -1, -1];
            const r = await testContract.mulDiv(new BigNumber(a), new BigNumber(n), new BigNumber(d)).callAsync();
            assertFixedEquals(r, fromFixed(1));
        });

        it('mulDiv(MIN_FIXED, int(-1), int(1)) throws', async () => {
            const [a, n, d] = [MIN_FIXED_VALUE, -1, 1];
            const expectedError = new FixedMathRevertErrors.BinOpError(
                FixedMathRevertErrors.BinOpErrorCodes.MultiplicationOverflow,
                a,
                n,
            );
            const tx = testContract.mulDiv(a, new BigNumber(n), new BigNumber(d)).callAsync();
            return expect(tx).to.revertWith(expectedError);
        });

        it('mulDiv(int(-1), MIN_FIXED, int(1)) throws', async () => {
            const [a, n, d] = [-1, MIN_FIXED_VALUE, 1];
            const expectedError = new FixedMathRevertErrors.BinOpError(
                FixedMathRevertErrors.BinOpErrorCodes.MultiplicationOverflow,
                a,
                n,
            );
            const tx = testContract.mulDiv(new BigNumber(a), n, new BigNumber(d)).callAsync();
            return expect(tx).to.revertWith(expectedError);
        });

        it('mulDiv(MIN_FIXED, int(1), int(-1)) throws', async () => {
            const [a, n, d] = [MIN_FIXED_VALUE, 1, -1];
            const expectedError = new FixedMathRevertErrors.BinOpError(
                FixedMathRevertErrors.BinOpErrorCodes.DivisionOverflow,
                a,
                d,
            );
            const tx = testContract.mulDiv(a, new BigNumber(n), new BigNumber(d)).callAsync();
            return expect(tx).to.revertWith(expectedError);
        });

        it('mulDiv(MAX_FIXED, int(-1), int(1)) == -MAX_FIXED', async () => {
            const [a, n, d] = [MAX_FIXED_VALUE, -1, 1];
            const r = await testContract.mulDiv(a, new BigNumber(n), new BigNumber(d)).callAsync();
            expect(r).to.bignumber.eq(MAX_FIXED_VALUE.negated());
        });

        it('mulDiv(MAX_FIXED, int(1), int(-1)) == -MAX_FIXED', async () => {
            const [a, n, d] = [MAX_FIXED_VALUE, 1, -1];
            const r = await testContract.mulDiv(a, new BigNumber(n), new BigNumber(d)).callAsync();
            expect(r).to.bignumber.eq(MAX_FIXED_VALUE.negated());
        });
    });

    describe('add()', () => {
        function add(a: Numberish, b: Numberish): BigNumber {
            return fromFixed(toFixed(a).plus(toFixed(b)));
        }

        it('0 + 0 == 0', async () => {
            const [a, b] = [0, 0];
            const r = await testContract.add(toFixed(a), toFixed(b)).callAsync();
            assertFixedEquals(r, 0);
        });

        it('adds two positive decimals', async () => {
            const [a, b] = ['9310841.31841', '491021921.318948193'];
            const r = await testContract.add(toFixed(a), toFixed(b)).callAsync();
            assertFixedEquals(r, add(a, b));
        });

        it('adds two mixed decimals', async () => {
            const [a, b] = ['9310841.31841', '-491021921.318948193'];
            const r = await testContract.add(toFixed(a), toFixed(b)).callAsync();
            assertFixedEquals(r, add(a, b));
        });

        it('throws on overflow', async () => {
            const [a, b] = [MAX_FIXED_VALUE, new BigNumber(1)];
            const expectedError = new FixedMathRevertErrors.BinOpError(
                FixedMathRevertErrors.BinOpErrorCodes.AdditionOverflow,
                a,
                b,
            );
            const tx = testContract.add(a, b).callAsync();
            return expect(tx).to.revertWith(expectedError);
        });

        it('throws on underflow', async () => {
            const [a, b] = [MIN_FIXED_VALUE, new BigNumber(-1)];
            const expectedError = new FixedMathRevertErrors.BinOpError(
                FixedMathRevertErrors.BinOpErrorCodes.AdditionOverflow,
                a,
                b,
            );
            const tx = testContract.add(a, b).callAsync();
            return expect(tx).to.revertWith(expectedError);
        });

        it('MIN_FIXED + MIN_FIXED throws', async () => {
            const [a, b] = [MIN_FIXED_VALUE, MIN_FIXED_VALUE];
            const expectedError = new FixedMathRevertErrors.BinOpError(
                FixedMathRevertErrors.BinOpErrorCodes.AdditionOverflow,
                a,
                b,
            );
            const tx = testContract.add(a, b).callAsync();
            return expect(tx).to.revertWith(expectedError);
        });

        it('MAX_FIXED + MAX_FIXED throws', async () => {
            const [a, b] = [MAX_FIXED_VALUE, MAX_FIXED_VALUE];
            const expectedError = new FixedMathRevertErrors.BinOpError(
                FixedMathRevertErrors.BinOpErrorCodes.AdditionOverflow,
                a,
                b,
            );
            const tx = testContract.add(a, b).callAsync();
            return expect(tx).to.revertWith(expectedError);
        });

        it('MIN_FIXED + MAX_FIXED == int(-1)', async () => {
            const [a, b] = [MIN_FIXED_VALUE, MAX_FIXED_VALUE];
            const r = await testContract.add(a, b).callAsync();
            expect(r).to.bignumber.eq(-1);
        });

        it('MAX_FIXED + (MIN_FIXED + int(1)) == 0', async () => {
            const [a, b] = [MAX_FIXED_VALUE, MIN_FIXED_VALUE.plus(1)];
            const r = await testContract.add(a, b).callAsync();
            expect(r).to.bignumber.eq(0);
        });
    });

    describe('sub()', () => {
        function sub(a: Numberish, b: Numberish): BigNumber {
            return fromFixed(toFixed(a).minus(toFixed(b)));
        }

        it('0 - 0 == 0', async () => {
            const [a, b] = [0, 0];
            const r = await testContract.sub(toFixed(a), toFixed(b)).callAsync();
            assertFixedEquals(r, 0);
        });

        it('subtracts two positive decimals', async () => {
            const [a, b] = ['9310841.31841', '491021921.318948193'];
            const r = await testContract.sub(toFixed(a), toFixed(b)).callAsync();
            assertFixedEquals(r, sub(a, b));
        });

        it('subtracts two mixed decimals', async () => {
            const [a, b] = ['9310841.31841', '-491021921.318948193'];
            const r = await testContract.sub(toFixed(a), toFixed(b)).callAsync();
            assertFixedEquals(r, sub(a, b));
        });

        it('throws on underflow', async () => {
            const [a, b] = [MIN_FIXED_VALUE, new BigNumber(1)];
            const expectedError = new FixedMathRevertErrors.BinOpError(
                FixedMathRevertErrors.BinOpErrorCodes.AdditionOverflow,
                a,
                b.negated(),
            );
            const tx = testContract.sub(a, b).callAsync();
            return expect(tx).to.revertWith(expectedError);
        });

        it('throws on overflow', async () => {
            const [a, b] = [MAX_FIXED_VALUE, new BigNumber(-1)];
            const expectedError = new FixedMathRevertErrors.BinOpError(
                FixedMathRevertErrors.BinOpErrorCodes.AdditionOverflow,
                a,
                b.negated(),
            );
            const tx = testContract.sub(a, b).callAsync();
            return expect(tx).to.revertWith(expectedError);
        });

        it('MIN_FIXED - MIN_FIXED throws', async () => {
            const [a, b] = [MIN_FIXED_VALUE, MIN_FIXED_VALUE];
            // This fails because `-MIN_FIXED_VALUE == MIN_FIXED_VALUE` because of
            // twos-complement.
            const expectedError = new FixedMathRevertErrors.SignedValueError(
                FixedMathRevertErrors.ValueErrorCodes.TooSmall,
                b,
            );
            const tx = testContract.sub(a, b).callAsync();
            return expect(tx).to.revertWith(expectedError);
        });

        it('MAX_FIXED - MAX_FIXED == 0', async () => {
            const [a, b] = [MAX_FIXED_VALUE, MAX_FIXED_VALUE];
            const r = await testContract.sub(a, b).callAsync();
            expect(r).to.bignumber.eq(0);
        });

        it('MIN_FIXED - MAX_FIXED throws', async () => {
            const [a, b] = [MIN_FIXED_VALUE, MAX_FIXED_VALUE];
            const expectedError = new FixedMathRevertErrors.BinOpError(
                FixedMathRevertErrors.BinOpErrorCodes.AdditionOverflow,
                a,
                b.negated(),
            );
            const tx = testContract.sub(a, b).callAsync();
            return expect(tx).to.revertWith(expectedError);
        });

        it('MAX_FIXED - MIN_FIXED throws', async () => {
            const [a, b] = [MAX_FIXED_VALUE, MIN_FIXED_VALUE];
            const expectedError = new FixedMathRevertErrors.SignedValueError(
                FixedMathRevertErrors.ValueErrorCodes.TooSmall,
                b,
            );
            const tx = testContract.sub(a, b).callAsync();
            return expect(tx).to.revertWith(expectedError);
        });
    });

    describe('mul()', () => {
        function mul(a: Numberish, b: Numberish): BigNumber {
            return fromFixed(
                toFixed(a)
                    .times(toFixed(b))
                    .dividedToIntegerBy(FIXED_POINT_DIVISOR),
            );
        }

        it('x * 0 == 0', async () => {
            const [a, b] = [1337, 0];
            const r = await testContract.mul(toFixed(a), toFixed(b)).callAsync();
            assertFixedEquals(r, b);
        });

        it('x * 1 == x', async () => {
            const [a, b] = [0.5, 1];
            const r = await testContract.mul(toFixed(a), toFixed(b)).callAsync();
            assertFixedEquals(r, a);
        });

        it('x * -1 == -x', async () => {
            const [a, b] = [0.5, -1];
            const r = await testContract.mul(toFixed(a), toFixed(b)).callAsync();
            assertFixedEquals(r, -a);
        });

        it('multiplies two positive decimals', async () => {
            const [a, b] = ['1.25394912112', '0.03413318948193'];
            const r = await testContract.mul(toFixed(a), toFixed(b)).callAsync();
            assertFixedEquals(r, mul(a, b));
        });

        it('multiplies two mixed decimals', async () => {
            const [a, b] = ['1.25394912112', '-0.03413318948193'];
            const r = await testContract.mul(toFixed(a), toFixed(b)).callAsync();
            assertFixedEquals(r, mul(a, b));
        });

        it('throws on underflow', async () => {
            const [a, b] = [MIN_FIXED_VALUE, new BigNumber(2)];
            const expectedError = new FixedMathRevertErrors.BinOpError(
                FixedMathRevertErrors.BinOpErrorCodes.MultiplicationOverflow,
                a,
                b,
            );
            const tx = testContract.mul(a, b).callAsync();
            return expect(tx).to.revertWith(expectedError);
        });

        it('throws on overflow', async () => {
            const [a, b] = [MAX_FIXED_VALUE, new BigNumber(2)];
            const expectedError = new FixedMathRevertErrors.BinOpError(
                FixedMathRevertErrors.BinOpErrorCodes.MultiplicationOverflow,
                a,
                b,
            );
            const tx = testContract.mul(a, b).callAsync();
            return expect(tx).to.revertWith(expectedError);
        });

        it('MAX_FIXED * int(1) == MAX_FIXED / FIXED_1', async () => {
            const [a, b] = [MAX_FIXED_VALUE, 1];
            const r = await testContract.mul(a, new BigNumber(b)).callAsync();
            expect(r).to.bignumber.eq(MAX_FIXED_VALUE.dividedToIntegerBy(FIXED_1));
        });

        it('MAX_FIXED * int(2) throws', async () => {
            const [a, b] = [MAX_FIXED_VALUE, 2];
            const expectedError = new FixedMathRevertErrors.BinOpError(
                FixedMathRevertErrors.BinOpErrorCodes.MultiplicationOverflow,
                a,
                b,
            );
            const tx = testContract.mul(a, new BigNumber(b)).callAsync();
            return expect(tx).to.revertWith(expectedError);
        });

        it('MAX_FIXED * MAX_FIXED throws', async () => {
            const [a, b] = [MAX_FIXED_VALUE, MAX_FIXED_VALUE];
            const expectedError = new FixedMathRevertErrors.BinOpError(
                FixedMathRevertErrors.BinOpErrorCodes.MultiplicationOverflow,
                a,
                b,
            );
            const tx = testContract.mul(a, b).callAsync();
            return expect(tx).to.revertWith(expectedError);
        });

        it('MIN_FIXED * MIN_FIXED throws', async () => {
            const [a, b] = [MIN_FIXED_VALUE, MIN_FIXED_VALUE];
            const expectedError = new FixedMathRevertErrors.BinOpError(
                FixedMathRevertErrors.BinOpErrorCodes.MultiplicationOverflow,
                a,
                b,
            );
            const tx = testContract.mul(a, b).callAsync();
            return expect(tx).to.revertWith(expectedError);
        });

        it('MAX_FIXED * MIN_FIXED throws', async () => {
            const [a, b] = [MAX_FIXED_VALUE, MIN_FIXED_VALUE];
            const expectedError = new FixedMathRevertErrors.BinOpError(
                FixedMathRevertErrors.BinOpErrorCodes.MultiplicationOverflow,
                a,
                b,
            );
            const tx = testContract.mul(a, b).callAsync();
            return expect(tx).to.revertWith(expectedError);
        });

        it('MIN_FIXED * int(-1) throws', async () => {
            const [a, b] = [MIN_FIXED_VALUE, -1];
            const expectedError = new FixedMathRevertErrors.BinOpError(
                FixedMathRevertErrors.BinOpErrorCodes.MultiplicationOverflow,
                a,
                b,
            );
            const tx = testContract.mul(a, new BigNumber(b)).callAsync();
            return expect(tx).to.revertWith(expectedError);
        });

        it('int(-1) * MIN_FIXED throws', async () => {
            const [a, b] = [-1, MIN_FIXED_VALUE];
            const expectedError = new FixedMathRevertErrors.BinOpError(
                FixedMathRevertErrors.BinOpErrorCodes.MultiplicationOverflow,
                a,
                b,
            );
            const tx = testContract.mul(new BigNumber(a), b).callAsync();
            return expect(tx).to.revertWith(expectedError);
        });

        it('MAX_FIXED * int(-1) == -MAX_FIXED / FIXED_1', async () => {
            const [a, b] = [MAX_FIXED_VALUE, -1];
            const r = await testContract.mul(a, new BigNumber(b)).callAsync();
            expect(r).to.bignumber.eq(MAX_FIXED_VALUE.negated().dividedToIntegerBy(FIXED_1));
        });
    });

    describe('div()', () => {
        function div(a: Numberish, b: Numberish): BigNumber {
            return fromFixed(
                toFixed(a)
                    .times(FIXED_POINT_DIVISOR)
                    .dividedBy(toFixed(b)),
            );
        }

        it('x / 0 throws', async () => {
            const [a, b] = [1, 0];
            const expectedError = new FixedMathRevertErrors.BinOpError(
                FixedMathRevertErrors.BinOpErrorCodes.DivisionByZero,
                toFixed(a).times(FIXED_POINT_DIVISOR),
                toFixed(b),
            );
            const tx = testContract.div(toFixed(a), toFixed(b)).callAsync();
            return expect(tx).to.revertWith(expectedError);
        });

        it('x / 1 == x', async () => {
            const [a, b] = [1.41214552, 1];
            const r = await testContract.div(toFixed(a), toFixed(b)).callAsync();
            assertFixedEquals(r, a);
        });

        it('x / -1 == -x', async () => {
            const [a, b] = [1.109312, -1];
            const r = await testContract.div(toFixed(a), toFixed(b)).callAsync();
            assertFixedEquals(r, -a);
        });

        it('divides two positive decimals', async () => {
            const [a, b] = ['1.25394912112', '0.03413318948193'];
            const r = await testContract.div(toFixed(a), toFixed(b)).callAsync();
            assertFixedEquals(r, div(a, b));
        });

        it('divides two mixed decimals', async () => {
            const [a, b] = ['1.25394912112', '-0.03413318948193'];
            const r = await testContract.div(toFixed(a), toFixed(b)).callAsync();
            assertFixedEquals(r, div(a, b));
        });

        it('MIN_FIXED / int(-1) throws', async () => {
            const [a, b] = [MIN_FIXED_VALUE, -1];
            const expectedError = new FixedMathRevertErrors.BinOpError(
                FixedMathRevertErrors.BinOpErrorCodes.MultiplicationOverflow,
                a,
                FIXED_1,
            );
            const tx = testContract.div(a, new BigNumber(b)).callAsync();
            return expect(tx).to.revertWith(expectedError);
        });

        it('MAX_FIXED / int(-1) throws', async () => {
            const [a, b] = [MIN_FIXED_VALUE, -1];
            const expectedError = new FixedMathRevertErrors.BinOpError(
                FixedMathRevertErrors.BinOpErrorCodes.MultiplicationOverflow,
                a,
                FIXED_1,
            );
            const tx = testContract.div(a, new BigNumber(b)).callAsync();
            return expect(tx).to.revertWith(expectedError);
        });

        it('int(-1) / MIN_FIXED == 0', async () => {
            const [a, b] = [-1, MIN_FIXED_VALUE];
            const r = await testContract.div(new BigNumber(a), b).callAsync();
            expect(r).to.bignumber.eq(0);
        });
    });

    describe('uintMul()', () => {
        it('0 * x == 0', async () => {
            const [a, b] = [0, 1234];
            const r = await testContract.uintMul(toFixed(a), new BigNumber(b)).callAsync();
            expect(r).to.bignumber.eq(0);
        });

        it('1 * x == int(x)', async () => {
            const [a, b] = [1, 1234];
            const r = await testContract.uintMul(toFixed(a), new BigNumber(b)).callAsync();
            expect(r).to.bignumber.eq(Math.trunc(b));
        });

        it('-1 * x == 0', async () => {
            const [a, b] = [-1, 1234];
            const r = await testContract.uintMul(toFixed(a), new BigNumber(b)).callAsync();
            expect(r).to.bignumber.eq(0);
        });

        it('0.5 * x == x/2', async () => {
            const [a, b] = [0.5, 1234];
            const r = await testContract.uintMul(toFixed(a), new BigNumber(b)).callAsync();
            expect(r).to.bignumber.eq(b / 2);
        });

        it('0.5 * x == 0 if x = 1', async () => {
            const [a, b] = [0.5, 1];
            const r = await testContract.uintMul(toFixed(a), new BigNumber(b)).callAsync();
            expect(r).to.bignumber.eq(0);
        });

        it('throws if rhs is too large', async () => {
            const [a, b] = [toFixed(1), MAX_FIXED_VALUE.plus(1)];
            const expectedError = new FixedMathRevertErrors.UnsignedValueError(
                FixedMathRevertErrors.ValueErrorCodes.TooLarge,
                b,
            );
            const tx = testContract.uintMul(a, b).callAsync();
            return expect(tx).to.revertWith(expectedError);
        });

        it('throws if lhs is too large', async () => {
            const [a, b] = [MAX_FIXED_VALUE, new BigNumber(2)];
            const expectedError = new FixedMathRevertErrors.BinOpError(
                FixedMathRevertErrors.BinOpErrorCodes.MultiplicationOverflow,
                a,
                b,
            );
            const tx = testContract.uintMul(a, b).callAsync();
            return expect(tx).to.revertWith(expectedError);
        });
    });

    describe('toInteger()', () => {
        it('toInteger(n) == int(n)', async () => {
            const n = 1337.5912;
            const r = await testContract.toInteger(toFixed(n)).callAsync();
            expect(r).to.bignumber.eq(Math.trunc(n));
        });

        it('toInteger(-n) == -int(n)', async () => {
            const n = -1337.5912;
            const r = await testContract.toInteger(toFixed(n)).callAsync();
            expect(r).to.bignumber.eq(Math.trunc(n));
        });

        it('toInteger(n) == 0, when 0 < n < 1', async () => {
            const n = 0.9995;
            const r = await testContract.toInteger(toFixed(n)).callAsync();
            expect(r).to.bignumber.eq(0);
        });

        it('toInteger(-n) == 0, when -1 < n < 0', async () => {
            const n = -0.9995;
            const r = await testContract.toInteger(toFixed(n)).callAsync();
            expect(r).to.bignumber.eq(0);
        });

        it('toInteger(0) == 0', async () => {
            const n = 0;
            const r = await testContract.toInteger(toFixed(n)).callAsync();
            expect(r).to.bignumber.eq(0);
        });
    });

    describe('toFixed()', () => {
        describe('signed', () => {
            it('converts a positive integer', async () => {
                const n = 1337;
                const r = await testContract.toFixedSigned1(new BigNumber(n)).callAsync();
                assertFixedEquals(r, n);
            });

            it('converts a negative integer', async () => {
                const n = -1337;
                const r = await testContract.toFixedSigned1(new BigNumber(n)).callAsync();
                assertFixedEquals(r, n);
            });

            it('converts a fraction with a positive numerator and denominator', async () => {
                const [n, d] = [1337, 1000];
                const r = await testContract.toFixedSigned2(new BigNumber(n), new BigNumber(d)).callAsync();
                assertFixedEquals(r, n / d);
            });

            it('converts a fraction with a negative numerator and positive denominator', async () => {
                const [n, d] = [-1337, 1000];
                const r = await testContract.toFixedSigned2(new BigNumber(n), new BigNumber(d)).callAsync();
                assertFixedEquals(r, n / d);
            });

            it('converts a fraction with a negative numerator and denominator', async () => {
                const [n, d] = [-1337, -1000];
                const r = await testContract.toFixedSigned2(new BigNumber(n), new BigNumber(d)).callAsync();
                assertFixedEquals(r, n / d);
            });

            it('converts a fraction with a negative numerator and negative denominator', async () => {
                const [n, d] = [-1337, -1000];
                const r = await testContract.toFixedSigned2(new BigNumber(n), new BigNumber(d)).callAsync();
                assertFixedEquals(r, n / d);
            });

            it('throws if the numerator is too large to convert', async () => {
                const [n, d] = [MAX_FIXED_VALUE.dividedToIntegerBy(FIXED_POINT_DIVISOR).plus(1), new BigNumber(1000)];
                const expectedError = new FixedMathRevertErrors.BinOpError(
                    FixedMathRevertErrors.BinOpErrorCodes.MultiplicationOverflow,
                    n,
                    FIXED_POINT_DIVISOR,
                );
                const tx = testContract.toFixedSigned2(n, d).callAsync();
                return expect(tx).to.revertWith(expectedError);
            });

            it('throws if the denominator is zero', async () => {
                const [n, d] = [new BigNumber(1), new BigNumber(0)];
                const expectedError = new FixedMathRevertErrors.BinOpError(
                    FixedMathRevertErrors.BinOpErrorCodes.DivisionByZero,
                    n.times(FIXED_POINT_DIVISOR),
                    d,
                );
                const tx = testContract.toFixedSigned2(n, d).callAsync();
                return expect(tx).to.revertWith(expectedError);
            });
        });

        describe('unsigned', () => {
            it('converts an integer', async () => {
                const n = 1337;
                const r = await testContract.toFixedUnsigned1(new BigNumber(n)).callAsync();
                assertFixedEquals(r, n);
            });

            it('converts a fraction', async () => {
                const [n, d] = [1337, 1000];
                const r = await testContract.toFixedUnsigned2(new BigNumber(n), new BigNumber(d)).callAsync();
                assertFixedEquals(r, n / d);
            });

            it('throws if the numerator is too large', async () => {
                const [n, d] = [MAX_FIXED_VALUE.plus(1), new BigNumber(1000)];
                const expectedError = new FixedMathRevertErrors.UnsignedValueError(
                    FixedMathRevertErrors.ValueErrorCodes.TooLarge,
                    n,
                );
                const tx = testContract.toFixedUnsigned2(n, d).callAsync();
                return expect(tx).to.revertWith(expectedError);
            });

            it('throws if the denominator is too large', async () => {
                const [n, d] = [new BigNumber(1000), MAX_FIXED_VALUE.plus(1)];
                const expectedError = new FixedMathRevertErrors.UnsignedValueError(
                    FixedMathRevertErrors.ValueErrorCodes.TooLarge,
                    d,
                );
                const tx = testContract.toFixedUnsigned2(n, d).callAsync();
                return expect(tx).to.revertWith(expectedError);
            });

            it('throws if the numerator is too large to convert', async () => {
                const [n, d] = [MAX_FIXED_VALUE.dividedToIntegerBy(FIXED_POINT_DIVISOR).plus(1), new BigNumber(1000)];
                const expectedError = new FixedMathRevertErrors.BinOpError(
                    FixedMathRevertErrors.BinOpErrorCodes.MultiplicationOverflow,
                    n,
                    FIXED_POINT_DIVISOR,
                );
                const tx = testContract.toFixedUnsigned2(n, d).callAsync();
                return expect(tx).to.revertWith(expectedError);
            });

            it('throws if the denominator is zero', async () => {
                const [n, d] = [new BigNumber(1), new BigNumber(0)];
                const expectedError = new FixedMathRevertErrors.BinOpError(
                    FixedMathRevertErrors.BinOpErrorCodes.DivisionByZero,
                    n.times(FIXED_POINT_DIVISOR),
                    d,
                );
                const tx = testContract.toFixedUnsigned2(n, d).callAsync();
                return expect(tx).to.revertWith(expectedError);
            });
        });
    });

    function getRandomDecimal(min: Numberish, max: Numberish): BigNumber {
        const range = new BigNumber(max).minus(min);
        const random = fromFixed(new BigNumber(hexRandom().substr(2), 16));
        return random.mod(range).plus(min);
    }

    describe('ln()', () => {
        const LN_PRECISION = 16;

        function ln(x: Numberish): BigNumber {
            return new BigNumber(
                toDecimal(x)
                    .ln()
                    .toFixed(128),
            );
        }

        it('ln(x = 0) throws', async () => {
            const x = toFixed(0);
            const expectedError = new FixedMathRevertErrors.SignedValueError(
                FixedMathRevertErrors.ValueErrorCodes.TooSmall,
                x,
            );
            const tx = testContract.ln(x).callAsync();
            return expect(tx).to.revertWith(expectedError);
        });

        it('ln(x > 1) throws', async () => {
            const x = toFixed(1.000001);
            const expectedError = new FixedMathRevertErrors.SignedValueError(
                FixedMathRevertErrors.ValueErrorCodes.TooLarge,
                x,
            );
            const tx = testContract.ln(x).callAsync();
            return expect(tx).to.revertWith(expectedError);
        });

        it('ln(x < 0) throws', async () => {
            const x = toFixed(-0.000001);
            const expectedError = new FixedMathRevertErrors.SignedValueError(
                FixedMathRevertErrors.ValueErrorCodes.TooSmall,
                x,
            );
            const tx = testContract.ln(x).callAsync();
            return expect(tx).to.revertWith(expectedError);
        });

        it('ln(x = 1) == 0', async () => {
            const x = toFixed(1);
            const r = await testContract.ln(x).callAsync();
            assertFixedEquals(r, 0);
        });

        it('ln(x < LN_MIN_VAL) == EXP_MIN_VAL', async () => {
            const x = toFixed(MIN_LN_NUMBER).minus(1);
            const r = await testContract.ln(x).callAsync();
            assertFixedEquals(r, MIN_EXP_NUMBER);
        });

        it('ln(x), where x is close to 0', async () => {
            const x = new BigNumber('1e-27');
            const r = await testContract.ln(toFixed(x)).callAsync();
            assertFixedRoughlyEquals(r, ln(x), 12);
        });

        it('ln(x), where x is close to 1', async () => {
            const x = new BigNumber(1).minus('1e-27');
            const r = await testContract.ln(toFixed(x)).callAsync();
            assertFixedRoughlyEquals(r, ln(x), LN_PRECISION);
        });

        it('ln(x = 0.85)', async () => {
            const x = 0.85;
            const r = await testContract.ln(toFixed(x)).callAsync();
            assertFixedRoughlyEquals(r, ln(x), LN_PRECISION);
        });

        blockchainTests.optional('fuzzing', () => {
            const inputs = _.times(FUZZ_COUNT, () => getRandomDecimal(0, 1));
            for (const x of inputs) {
                it(`ln(${x.toString(10)})`, async () => {
                    const r = await testContract.ln(toFixed(x)).callAsync();
                    assertFixedRoughlyEquals(r, ln(x), LN_PRECISION);
                });
            }
        });
    });

    describe('exp()', () => {
        const EXP_PRECISION = 18;

        function exp(x: Numberish): BigNumber {
            return new BigNumber(
                toDecimal(x)
                    .exp()
                    .toFixed(128),
            );
        }

        it('exp(x = 0) == 1', async () => {
            const x = toFixed(0);
            const r = await testContract.exp(x).callAsync();
            assertFixedEquals(r, 1);
        });

        it('exp(x > EXP_MAX_VAL) throws', async () => {
            const x = toFixed(MAX_EXP_NUMBER).plus(1);
            const expectedError = new FixedMathRevertErrors.SignedValueError(
                FixedMathRevertErrors.ValueErrorCodes.TooLarge,
                x,
            );
            const tx = testContract.exp(x).callAsync();
            return expect(tx).to.revertWith(expectedError);
        });

        it('exp(x < EXP_MIN_VAL) == 0', async () => {
            const x = toFixed(MIN_EXP_NUMBER).minus(1);
            const r = await testContract.exp(x).callAsync();
            assertFixedEquals(r, 0);
        });

        it('exp(x < 0), where x is close to 0', async () => {
            const x = new BigNumber('-1e-18');
            const r = await testContract.exp(toFixed(x)).callAsync();
            assertFixedRoughlyEquals(r, exp(x), EXP_PRECISION);
        });

        it('exp(x), where x is close to EXP_MIN_VAL', async () => {
            const x = MIN_EXP_NUMBER.plus('1e-18');
            const r = await testContract.exp(toFixed(x)).callAsync();
            assertFixedRoughlyEquals(r, exp(x), EXP_PRECISION);
        });

        it('exp(x = -0.85)', async () => {
            const x = -0.85;
            const r = await testContract.exp(toFixed(x)).callAsync();
            assertFixedRoughlyEquals(r, exp(x), EXP_PRECISION);
        });

        blockchainTests.optional('fuzzing', () => {
            const inputs = _.times(FUZZ_COUNT, () => getRandomDecimal(MIN_EXP_NUMBER, MAX_EXP_NUMBER));
            for (const x of inputs) {
                it(`exp(${x.toString(10)})`, async () => {
                    const r = await testContract.exp(toFixed(x)).callAsync();
                    assertFixedRoughlyEquals(r, exp(x), EXP_PRECISION);
                });
            }
        });
    });
});
// tslint:disable-next-line: max-file-line-count
