import { ECSignature, SignedOrder } from '@0xproject/types';
import { BigNumber } from '@0xproject/utils';
import * as chai from 'chai';
import 'mocha';

import { ZeroEx } from '../src/0x';
import { RemainingFillableCalculator } from '../src/order_watcher/remaining_fillable_calculator';

import { chaiSetup } from './utils/chai_setup';

chaiSetup.configure();
const expect = chai.expect;

describe('RemainingFillableCalculator', () => {
    let calculator: RemainingFillableCalculator;
    let signedOrder: SignedOrder;
    let transferrableMakerTokenAmount: BigNumber;
    let transferrableMakerFeeTokenAmount: BigNumber;
    let remainingMakerTokenAmount: BigNumber;
    let makerAmount: BigNumber;
    let takerAmount: BigNumber;
    let makerFeeAmount: BigNumber;
    let isMakerTokenZRX: boolean;
    const makerToken: string = '0x1';
    const takerToken: string = '0x2';
    const decimals: number = 4;
    const zero: BigNumber = new BigNumber(0);
    const zeroAddress = '0x0';
    const signature: ECSignature = { v: 27, r: '', s: '' };
    beforeEach(async () => {
        [makerAmount, takerAmount, makerFeeAmount] = [
            ZeroEx.toBaseUnitAmount(new BigNumber(50), decimals),
            ZeroEx.toBaseUnitAmount(new BigNumber(5), decimals),
            ZeroEx.toBaseUnitAmount(new BigNumber(1), decimals),
        ];
        [transferrableMakerTokenAmount, transferrableMakerFeeTokenAmount] = [
            ZeroEx.toBaseUnitAmount(new BigNumber(50), decimals),
            ZeroEx.toBaseUnitAmount(new BigNumber(5), decimals),
        ];
    });
    function buildSignedOrder(): SignedOrder {
        return {
            ecSignature: signature,
            exchangeContractAddress: zeroAddress,
            feeRecipient: zeroAddress,
            maker: zeroAddress,
            taker: zeroAddress,
            makerFee: makerFeeAmount,
            takerFee: zero,
            makerTokenAmount: makerAmount,
            takerTokenAmount: takerAmount,
            makerTokenAddress: makerToken,
            takerTokenAddress: takerToken,
            salt: zero,
            expirationUnixTimestampSec: zero,
        };
    }
    describe('Maker token is NOT ZRX', () => {
        before(async () => {
            isMakerTokenZRX = false;
        });
        it('calculates the correct amount when unfilled and funds available', () => {
            signedOrder = buildSignedOrder();
            remainingMakerTokenAmount = signedOrder.makerTokenAmount;
            calculator = new RemainingFillableCalculator(
                signedOrder,
                isMakerTokenZRX,
                transferrableMakerTokenAmount,
                transferrableMakerFeeTokenAmount,
                remainingMakerTokenAmount,
            );
            expect(calculator.computeRemainingMakerFillable()).to.be.bignumber.equal(remainingMakerTokenAmount);
        });
        it('calculates the correct amount when partially filled and funds available', () => {
            signedOrder = buildSignedOrder();
            remainingMakerTokenAmount = ZeroEx.toBaseUnitAmount(new BigNumber(1), decimals);
            calculator = new RemainingFillableCalculator(
                signedOrder,
                isMakerTokenZRX,
                transferrableMakerTokenAmount,
                transferrableMakerFeeTokenAmount,
                remainingMakerTokenAmount,
            );
            expect(calculator.computeRemainingMakerFillable()).to.be.bignumber.equal(remainingMakerTokenAmount);
        });
        it('calculates the amount to be 0 when all fee funds are transferred', () => {
            signedOrder = buildSignedOrder();
            transferrableMakerFeeTokenAmount = zero;
            remainingMakerTokenAmount = signedOrder.makerTokenAmount;
            calculator = new RemainingFillableCalculator(
                signedOrder,
                isMakerTokenZRX,
                transferrableMakerTokenAmount,
                transferrableMakerFeeTokenAmount,
                remainingMakerTokenAmount,
            );
            expect(calculator.computeRemainingMakerFillable()).to.be.bignumber.equal(zero);
        });
        it('calculates the correct amount when balance is less than remaining fillable', () => {
            signedOrder = buildSignedOrder();
            const partiallyFilledAmount = ZeroEx.toBaseUnitAmount(new BigNumber(2), decimals);
            remainingMakerTokenAmount = signedOrder.makerTokenAmount.minus(partiallyFilledAmount);
            transferrableMakerTokenAmount = remainingMakerTokenAmount.minus(partiallyFilledAmount);
            calculator = new RemainingFillableCalculator(
                signedOrder,
                isMakerTokenZRX,
                transferrableMakerTokenAmount,
                transferrableMakerFeeTokenAmount,
                remainingMakerTokenAmount,
            );
            expect(calculator.computeRemainingMakerFillable()).to.be.bignumber.equal(transferrableMakerTokenAmount);
        });
        describe('Order to Fee Ratio is < 1', () => {
            beforeEach(async () => {
                [makerAmount, takerAmount, makerFeeAmount] = [
                    ZeroEx.toBaseUnitAmount(new BigNumber(3), decimals),
                    ZeroEx.toBaseUnitAmount(new BigNumber(6), decimals),
                    ZeroEx.toBaseUnitAmount(new BigNumber(6), decimals),
                ];
            });
            it('calculates the correct amount when funds unavailable', () => {
                signedOrder = buildSignedOrder();
                remainingMakerTokenAmount = signedOrder.makerTokenAmount;
                const transferredAmount = ZeroEx.toBaseUnitAmount(new BigNumber(2), decimals);
                transferrableMakerTokenAmount = remainingMakerTokenAmount.minus(transferredAmount);
                calculator = new RemainingFillableCalculator(
                    signedOrder,
                    isMakerTokenZRX,
                    transferrableMakerTokenAmount,
                    transferrableMakerFeeTokenAmount,
                    remainingMakerTokenAmount,
                );
                expect(calculator.computeRemainingMakerFillable()).to.be.bignumber.equal(transferrableMakerTokenAmount);
            });
        });
        describe('Ratio is not evenly divisble', () => {
            beforeEach(async () => {
                [makerAmount, takerAmount, makerFeeAmount] = [
                    ZeroEx.toBaseUnitAmount(new BigNumber(3), decimals),
                    ZeroEx.toBaseUnitAmount(new BigNumber(7), decimals),
                    ZeroEx.toBaseUnitAmount(new BigNumber(7), decimals),
                ];
            });
            it('calculates the correct amount when funds unavailable', () => {
                signedOrder = buildSignedOrder();
                remainingMakerTokenAmount = signedOrder.makerTokenAmount;
                const transferredAmount = ZeroEx.toBaseUnitAmount(new BigNumber(2), decimals);
                transferrableMakerTokenAmount = remainingMakerTokenAmount.minus(transferredAmount);
                calculator = new RemainingFillableCalculator(
                    signedOrder,
                    isMakerTokenZRX,
                    transferrableMakerTokenAmount,
                    transferrableMakerFeeTokenAmount,
                    remainingMakerTokenAmount,
                );
                const calculatedFillableAmount = calculator.computeRemainingMakerFillable();
                expect(calculatedFillableAmount.lessThanOrEqualTo(transferrableMakerTokenAmount)).to.be.true();
                expect(calculatedFillableAmount).to.be.bignumber.greaterThan(new BigNumber(0));
                const orderToFeeRatio = signedOrder.makerTokenAmount.dividedBy(signedOrder.makerFee);
                const calculatedFeeAmount = calculatedFillableAmount.dividedBy(orderToFeeRatio);
                expect(calculatedFeeAmount).to.be.bignumber.lessThan(transferrableMakerFeeTokenAmount);
            });
        });
    });
    describe('Maker Token is ZRX', () => {
        before(async () => {
            isMakerTokenZRX = true;
        });
        it('calculates the correct amount when unfilled and funds available', () => {
            signedOrder = buildSignedOrder();
            transferrableMakerTokenAmount = makerAmount.plus(makerFeeAmount);
            transferrableMakerFeeTokenAmount = transferrableMakerTokenAmount;
            remainingMakerTokenAmount = signedOrder.makerTokenAmount;
            calculator = new RemainingFillableCalculator(
                signedOrder,
                isMakerTokenZRX,
                transferrableMakerTokenAmount,
                transferrableMakerFeeTokenAmount,
                remainingMakerTokenAmount,
            );
            expect(calculator.computeRemainingMakerFillable()).to.be.bignumber.equal(remainingMakerTokenAmount);
        });
        it('calculates the correct amount when partially filled and funds available', () => {
            signedOrder = buildSignedOrder();
            remainingMakerTokenAmount = ZeroEx.toBaseUnitAmount(new BigNumber(1), decimals);
            calculator = new RemainingFillableCalculator(
                signedOrder,
                isMakerTokenZRX,
                transferrableMakerTokenAmount,
                transferrableMakerFeeTokenAmount,
                remainingMakerTokenAmount,
            );
            expect(calculator.computeRemainingMakerFillable()).to.be.bignumber.equal(remainingMakerTokenAmount);
        });
        it('calculates the amount to be 0 when all fee funds are transferred', () => {
            signedOrder = buildSignedOrder();
            transferrableMakerTokenAmount = zero;
            transferrableMakerFeeTokenAmount = zero;
            remainingMakerTokenAmount = signedOrder.makerTokenAmount;
            calculator = new RemainingFillableCalculator(
                signedOrder,
                isMakerTokenZRX,
                transferrableMakerTokenAmount,
                transferrableMakerFeeTokenAmount,
                remainingMakerTokenAmount,
            );
            expect(calculator.computeRemainingMakerFillable()).to.be.bignumber.equal(zero);
        });
        it('calculates the correct amount when balance is less than remaining fillable', () => {
            signedOrder = buildSignedOrder();
            const partiallyFilledAmount = ZeroEx.toBaseUnitAmount(new BigNumber(2), decimals);
            remainingMakerTokenAmount = signedOrder.makerTokenAmount.minus(partiallyFilledAmount);
            transferrableMakerTokenAmount = remainingMakerTokenAmount.minus(partiallyFilledAmount);
            transferrableMakerFeeTokenAmount = transferrableMakerTokenAmount;

            const orderToFeeRatio = signedOrder.makerTokenAmount.dividedToIntegerBy(signedOrder.makerFee);
            const expectedFillableAmount = new BigNumber(450980);
            calculator = new RemainingFillableCalculator(
                signedOrder,
                isMakerTokenZRX,
                transferrableMakerTokenAmount,
                transferrableMakerFeeTokenAmount,
                remainingMakerTokenAmount,
            );
            const calculatedFillableAmount = calculator.computeRemainingMakerFillable();
            const numberOfFillsInRatio = calculatedFillableAmount.dividedToIntegerBy(orderToFeeRatio);
            const calculatedFillableAmountPlusFees = calculatedFillableAmount.plus(numberOfFillsInRatio);
            expect(calculatedFillableAmountPlusFees).to.be.bignumber.lessThan(transferrableMakerTokenAmount);
            expect(calculatedFillableAmountPlusFees).to.be.bignumber.lessThan(remainingMakerTokenAmount);
            expect(calculatedFillableAmount).to.be.bignumber.equal(expectedFillableAmount);
            expect(numberOfFillsInRatio.decimalPlaces()).to.be.equal(0);
        });
    });
});
