import {
    blockchainTests,
    constants,
    expect,
    getRandomInteger,
    randomAddress,
    verifyEventsFromLogs,
} from '@0x/contracts-test-utils';
import { hexUtils, OwnableRevertErrors, StringRevertError, ZeroExRevertErrors } from '@0x/utils';

import { artifacts } from './artifacts';
import { FlashWalletContract, TestCallTargetContract, TestCallTargetEvents } from './wrappers';

blockchainTests.resets('FlashWallet', env => {
    let owner: string;
    let wallet: FlashWalletContract;
    let callTarget: TestCallTargetContract;

    before(async () => {
        [owner] = await env.getAccountAddressesAsync();
        wallet = await FlashWalletContract.deployFrom0xArtifactAsync(
            artifacts.FlashWallet,
            env.provider,
            {
                ...env.txDefaults,
                from: owner,
            },
            artifacts,
        );
        callTarget = await TestCallTargetContract.deployFrom0xArtifactAsync(
            artifacts.TestCallTarget,
            env.provider,
            env.txDefaults,
            artifacts,
        );
    });

    const TARGET_RETURN_VALUE = hexUtils.rightPad('0x12345678');
    const REVERTING_DATA = '0x1337';

    it('owned by deployer', () => {
        return expect(wallet.owner().callAsync()).to.eventually.eq(owner);
    });

    describe('executeCall()', () => {
        it('non-owner cannot call executeCall()', async () => {
            const notOwner = randomAddress();
            const tx = wallet
                .executeCall(randomAddress(), hexUtils.random(), getRandomInteger(0, '100e18'))
                .callAsync({ from: notOwner });
            return expect(tx).to.revertWith(new OwnableRevertErrors.OnlyOwnerError(notOwner));
        });

        it('owner can call executeCall()', async () => {
            const targetData = hexUtils.random(128);
            const receipt = await wallet
                .executeCall(callTarget.address, targetData, constants.ZERO_AMOUNT)
                .awaitTransactionSuccessAsync({ from: owner });
            verifyEventsFromLogs(
                receipt.logs,
                [
                    {
                        context: callTarget.address,
                        sender: wallet.address,
                        data: targetData,
                        value: constants.ZERO_AMOUNT,
                    },
                ],
                TestCallTargetEvents.CallTargetCalled,
            );
        });

        it('owner can call executeCall() with attached ETH', async () => {
            const targetData = hexUtils.random(128);
            const callValue = getRandomInteger(1, '1e18');
            const receipt = await wallet
                .executeCall(callTarget.address, targetData, callValue)
                .awaitTransactionSuccessAsync({ from: owner, value: callValue });
            verifyEventsFromLogs(
                receipt.logs,
                [
                    {
                        context: callTarget.address,
                        sender: wallet.address,
                        data: targetData,
                        value: callValue,
                    },
                ],
                TestCallTargetEvents.CallTargetCalled,
            );
        });

        it('owner can call executeCall() can transfer less ETH than attached', async () => {
            const targetData = hexUtils.random(128);
            const callValue = getRandomInteger(1, '1e18');
            const receipt = await wallet
                .executeCall(callTarget.address, targetData, callValue.minus(1))
                .awaitTransactionSuccessAsync({ from: owner, value: callValue });
            verifyEventsFromLogs(
                receipt.logs,
                [
                    {
                        context: callTarget.address,
                        sender: wallet.address,
                        data: targetData,
                        value: callValue.minus(1),
                    },
                ],
                TestCallTargetEvents.CallTargetCalled,
            );
        });

        it('wallet returns call result', async () => {
            const result = await wallet
                .executeCall(callTarget.address, hexUtils.random(128), constants.ZERO_AMOUNT)
                .callAsync({ from: owner });
            expect(result).to.eq(TARGET_RETURN_VALUE);
        });

        it('wallet wraps call revert', async () => {
            const tx = wallet
                .executeCall(callTarget.address, REVERTING_DATA, constants.ZERO_AMOUNT)
                .callAsync({ from: owner });
            return expect(tx).to.revertWith(
                new ZeroExRevertErrors.Wallet.WalletExecuteCallFailedError(
                    wallet.address,
                    callTarget.address,
                    REVERTING_DATA,
                    constants.ZERO_AMOUNT,
                    new StringRevertError('TestCallTarget/REVERT'),
                ),
            );
        });

        it('wallet can receive ETH', async () => {
            await env.web3Wrapper.sendTransactionAsync({
                to: wallet.address,
                from: owner,
                value: 1,
            });
            const bal = await env.web3Wrapper.getBalanceInWeiAsync(wallet.address);
            expect(bal).to.bignumber.eq(1);
        });
    });

    describe('executeDelegateCall()', () => {
        it('non-owner cannot call executeDelegateCall()', async () => {
            const notOwner = randomAddress();
            const tx = wallet.executeDelegateCall(randomAddress(), hexUtils.random()).callAsync({ from: notOwner });
            return expect(tx).to.revertWith(new OwnableRevertErrors.OnlyOwnerError(notOwner));
        });

        it('owner can call executeDelegateCall()', async () => {
            const targetData = hexUtils.random(128);
            const receipt = await wallet
                .executeDelegateCall(callTarget.address, targetData)
                .awaitTransactionSuccessAsync({ from: owner });
            verifyEventsFromLogs(
                receipt.logs,
                [
                    {
                        context: wallet.address,
                        sender: owner,
                        data: targetData,
                        value: constants.ZERO_AMOUNT,
                    },
                ],
                TestCallTargetEvents.CallTargetCalled,
            );
        });

        it('executeDelegateCall() is payable', async () => {
            const targetData = hexUtils.random(128);
            const callValue = getRandomInteger(1, '1e18');
            const receipt = await wallet
                .executeDelegateCall(callTarget.address, targetData)
                .awaitTransactionSuccessAsync({ from: owner, value: callValue });
            verifyEventsFromLogs(
                receipt.logs,
                [
                    {
                        context: wallet.address,
                        sender: owner,
                        data: targetData,
                        value: callValue,
                    },
                ],
                TestCallTargetEvents.CallTargetCalled,
            );
        });

        it('wallet returns call result', async () => {
            const result = await wallet
                .executeDelegateCall(callTarget.address, hexUtils.random(128))
                .callAsync({ from: owner });
            expect(result).to.eq(TARGET_RETURN_VALUE);
        });

        it('wallet wraps call revert', async () => {
            const tx = wallet.executeDelegateCall(callTarget.address, REVERTING_DATA).callAsync({ from: owner });
            return expect(tx).to.revertWith(
                new ZeroExRevertErrors.Wallet.WalletExecuteDelegateCallFailedError(
                    wallet.address,
                    callTarget.address,
                    REVERTING_DATA,
                    new StringRevertError('TestCallTarget/REVERT'),
                ),
            );
        });
    });
});
