import {
    artifacts as assetProxyArtifacts,
    ERC1155ProxyContract,
    ERC20ProxyContract,
    ERC721ProxyContract,
    MultiAssetProxyContract,
    StaticCallProxyContract,
} from '@0x/contracts-asset-proxy';
import {
    artifacts as exchangeArtifacts,
    ExchangeAssetProxyRegisteredEventArgs,
    ExchangeContract,
    ExchangeEvents,
    ExchangeProtocolFeeCollectorAddressEventArgs,
    ExchangeProtocolFeeMultiplierEventArgs,
} from '@0x/contracts-exchange';
import { artifacts as multisigArtifacts, ZeroExGovernorContract } from '@0x/contracts-multisig';
import {
    artifacts as stakingArtifacts,
    constants as stakingConstants,
    StakingContract,
    StakingEvents,
    StakingExchangeAddedEventArgs,
    StakingProxyContract,
} from '@0x/contracts-staking';
import { blockchainTests, constants, expect, filterLogsToArguments } from '@0x/contracts-test-utils';
import {
    AuthorizableAuthorizedAddressAddedEventArgs,
    AuthorizableAuthorizedAddressRemovedEventArgs,
    AuthorizableEvents,
} from '@0x/contracts-utils';
import { AssetProxyId } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { TxData } from 'ethereum-types';

import { AssetProxyDispatcher, Authorizable, Ownable } from './framework/wrapper_interfaces';

// tslint:disable:no-unnecessary-type-assertion
blockchainTests('Deployment and Configuration End to End Tests', env => {
    // Available Addresses
    let owner: string;

    // Contract Instances
    let governor: ZeroExGovernorContract;
    let erc20Proxy: ERC20ProxyContract;
    let erc721Proxy: ERC721ProxyContract;
    let erc1155Proxy: ERC1155ProxyContract;
    let exchange: ExchangeContract;
    let multiAssetProxy: MultiAssetProxyContract;
    let staking: StakingContract;
    let staticCallProxy: StaticCallProxyContract;
    let stakingProxy: StakingProxyContract;
    let stakingWrapper: StakingContract;

    // TxDefaults
    let txDefaults: Partial<TxData>;

    // ChainId of the Exchange
    let chainId: number;

    // Protocol Fees
    const protocolFeeMultiplier = new BigNumber(150000);

    before(async () => {
        // Get the chain ID.
        chainId = await env.getChainIdAsync();

        // Create accounts and tx defaults
        [owner] = await env.getAccountAddressesAsync();
        txDefaults = {
            ...env.txDefaults,
            from: owner,
        };

        // Deploy ZeroExGovernor. For the purposes of this test, we will assume that
        // the ZeroExGovernor does not know what destinations will be needed during
        // construction.
        governor = await ZeroExGovernorContract.deployFrom0xArtifactAsync(
            multisigArtifacts.ZeroExGovernor,
            env.provider,
            txDefaults,
            multisigArtifacts,
            [],
            [],
            [],
            [owner],
            new BigNumber(1),
            constants.ZERO_AMOUNT,
        );

        // Deploy Exchange.
        exchange = await ExchangeContract.deployFrom0xArtifactAsync(
            exchangeArtifacts.Exchange,
            env.provider,
            txDefaults,
            exchangeArtifacts,
            new BigNumber(chainId),
        );

        // Deploy Staking.
        staking = await StakingContract.deployFrom0xArtifactAsync(
            stakingArtifacts.Staking,
            env.provider,
            txDefaults,
            stakingArtifacts,
        );

        // Deploy the staking proxy.
        stakingProxy = await StakingProxyContract.deployFrom0xArtifactAsync(
            stakingArtifacts.StakingProxy,
            env.provider,
            txDefaults,
            stakingArtifacts,
            staking.address,
        );

        // Authorize owner in the staking proxy.
        await stakingProxy.addAuthorizedAddress(owner).awaitTransactionSuccessAsync();

        // Deploy the asset proxy contracts.
        erc20Proxy = await ERC20ProxyContract.deployFrom0xArtifactAsync(
            assetProxyArtifacts.ERC20Proxy,
            env.provider,
            txDefaults,
            assetProxyArtifacts,
        );
        erc721Proxy = await ERC721ProxyContract.deployFrom0xArtifactAsync(
            assetProxyArtifacts.ERC721Proxy,
            env.provider,
            txDefaults,
            assetProxyArtifacts,
        );
        erc1155Proxy = await ERC1155ProxyContract.deployFrom0xArtifactAsync(
            assetProxyArtifacts.ERC1155Proxy,
            env.provider,
            txDefaults,
            assetProxyArtifacts,
        );
        multiAssetProxy = await MultiAssetProxyContract.deployFrom0xArtifactAsync(
            assetProxyArtifacts.MultiAssetProxy,
            env.provider,
            txDefaults,
            assetProxyArtifacts,
        );
        staticCallProxy = await StaticCallProxyContract.deployFrom0xArtifactAsync(
            assetProxyArtifacts.StaticCallProxy,
            env.provider,
            txDefaults,
            assetProxyArtifacts,
        );

        // Set up the staking wrapper so that the entire staking interface can be accessed
        // easily through the proxy.
        stakingWrapper = new StakingContract(stakingProxy.address, env.provider);
    });

    describe('deployment and configuration', () => {
        describe('exchange specific', () => {
            // Registers an asset proxy in the exchange contract and ensure that the correct state changes occurred.
            async function registerAssetProxyAndAssertSuccessAsync(
                registrationContract: AssetProxyDispatcher,
                assetProxyAddress: string,
                assetProxyId: string,
            ): Promise<void> {
                // Register the asset proxy.
                const receipt = await registrationContract
                    .registerAssetProxy(assetProxyAddress)
                    .awaitTransactionSuccessAsync({
                        from: owner,
                    });

                // Ensure that the correct event was logged.
                const logs = filterLogsToArguments<ExchangeAssetProxyRegisteredEventArgs>(
                    receipt.logs,
                    ExchangeEvents.AssetProxyRegistered,
                );
                expect(logs).to.be.deep.eq([{ id: assetProxyId, assetProxy: assetProxyAddress }]);

                // Ensure that the asset proxy was actually registered.
                const proxyAddress = await registrationContract.getAssetProxy(assetProxyId).callAsync();
                expect(proxyAddress).to.be.eq(assetProxyAddress);
            }

            // Authorizes an address for a given asset proxy using the owner address.
            async function authorizeAddressAndAssertSuccessAsync(
                authorizable: Authorizable,
                newAuthorityAddress: string,
            ): Promise<void> {
                // Authorize the address.
                const receipt = await authorizable
                    .addAuthorizedAddress(newAuthorityAddress)
                    .awaitTransactionSuccessAsync({ from: owner });

                // Ensure that the correct log was emitted.
                const logs = filterLogsToArguments<AuthorizableAuthorizedAddressAddedEventArgs>(
                    receipt.logs,
                    AuthorizableEvents.AuthorizedAddressAdded,
                );
                expect(logs).to.be.deep.eq([{ target: newAuthorityAddress, caller: owner }]);

                // Ensure that the address was actually authorized.
                const wasAuthorized = await authorizable.authorized(newAuthorityAddress).callAsync();
                expect(wasAuthorized).to.be.true();
            }

            it('should successfully register the asset proxies in the exchange', async () => {
                // Register the asset proxies in the exchange.
                await registerAssetProxyAndAssertSuccessAsync(exchange, erc20Proxy.address, AssetProxyId.ERC20);
                await registerAssetProxyAndAssertSuccessAsync(exchange, erc721Proxy.address, AssetProxyId.ERC721);
                await registerAssetProxyAndAssertSuccessAsync(exchange, erc1155Proxy.address, AssetProxyId.ERC1155);
                await registerAssetProxyAndAssertSuccessAsync(
                    exchange,
                    multiAssetProxy.address,
                    AssetProxyId.MultiAsset,
                );
                await registerAssetProxyAndAssertSuccessAsync(
                    exchange,
                    staticCallProxy.address,
                    AssetProxyId.StaticCall,
                );
            });

            it('should successfully register the asset proxies in the multi-asset proxy', async () => {
                // Register the asset proxies in the multi-asset proxy.
                await registerAssetProxyAndAssertSuccessAsync(multiAssetProxy, erc20Proxy.address, AssetProxyId.ERC20);
                await registerAssetProxyAndAssertSuccessAsync(
                    multiAssetProxy,
                    erc721Proxy.address,
                    AssetProxyId.ERC721,
                );
                await registerAssetProxyAndAssertSuccessAsync(
                    multiAssetProxy,
                    erc1155Proxy.address,
                    AssetProxyId.ERC1155,
                );
                await registerAssetProxyAndAssertSuccessAsync(
                    multiAssetProxy,
                    staticCallProxy.address,
                    AssetProxyId.StaticCall,
                );
            });

            it('should successfully add the exchange as an authority in the appropriate asset proxies', async () => {
                // Authorize the exchange in all of the asset proxies, except for the static call proxy.
                await authorizeAddressAndAssertSuccessAsync(erc20Proxy, exchange.address);
                await authorizeAddressAndAssertSuccessAsync(erc721Proxy, exchange.address);
                await authorizeAddressAndAssertSuccessAsync(erc1155Proxy, exchange.address);
                await authorizeAddressAndAssertSuccessAsync(multiAssetProxy, exchange.address);
            });

            it('should successfully add the multi asset proxy as an authority in the appropriate asset proxies', async () => {
                // Authorize the multi-asset proxy in the token asset proxies.
                await authorizeAddressAndAssertSuccessAsync(erc20Proxy, multiAssetProxy.address);
                await authorizeAddressAndAssertSuccessAsync(erc721Proxy, multiAssetProxy.address);
                await authorizeAddressAndAssertSuccessAsync(erc1155Proxy, multiAssetProxy.address);
            });
        });

        describe('staking specific', () => {
            it('should have properly configured the staking proxy with the logic contract', async () => {
                // Ensure that the registered staking contract is correct.
                const stakingAddress = await stakingProxy.stakingContract().callAsync();
                expect(stakingAddress).to.be.eq(staking.address);
            });

            it('should have initialized the correct parameters in the staking proxy', async () => {
                // Ensure that the correct parameters were set.
                const params = await stakingWrapper.getParams().callAsync();
                expect(params).to.be.deep.eq([
                    stakingConstants.DEFAULT_PARAMS.epochDurationInSeconds,
                    stakingConstants.DEFAULT_PARAMS.rewardDelegatedStakeWeight,
                    stakingConstants.DEFAULT_PARAMS.minimumPoolStake,
                    stakingConstants.DEFAULT_PARAMS.cobbDouglasAlphaNumerator,
                    stakingConstants.DEFAULT_PARAMS.cobbDouglasAlphaDenominator,
                ]);
            });
        });

        describe('exchange and staking integration', () => {
            it('should successfully register the exchange in the staking contract', async () => {
                // Register the exchange.
                const receipt = await stakingWrapper.addExchangeAddress(exchange.address).awaitTransactionSuccessAsync({
                    from: owner,
                });

                // Ensure that the correct events were logged.
                const logs = filterLogsToArguments<StakingExchangeAddedEventArgs>(
                    receipt.logs,
                    StakingEvents.ExchangeAdded,
                );
                expect(logs).to.be.deep.eq([{ exchangeAddress: exchange.address }]);

                // Ensure that the exchange was registered.
                const wasRegistered = await stakingWrapper.validExchanges(exchange.address).callAsync();
                expect(wasRegistered).to.be.true();
            });

            it('should successfully register the staking contract in the exchange', async () => {
                // Register the staking contract.
                const receipt = await exchange
                    .setProtocolFeeCollectorAddress(stakingProxy.address)
                    .awaitTransactionSuccessAsync({
                        from: owner,
                    });

                // Ensure that the correct events were logged.
                const logs = filterLogsToArguments<ExchangeProtocolFeeCollectorAddressEventArgs>(
                    receipt.logs,
                    ExchangeEvents.ProtocolFeeCollectorAddress,
                );
                expect(logs).to.be.deep.eq([
                    {
                        oldProtocolFeeCollector: constants.NULL_ADDRESS,
                        updatedProtocolFeeCollector: stakingProxy.address,
                    },
                ]);

                // Ensure that the staking contract was registered.
                const feeCollector = await exchange.protocolFeeCollector().callAsync();
                expect(feeCollector).to.be.eq(stakingProxy.address);
            });

            it('should successfully update the protocol fee multiplier in the staking contract', async () => {
                // Update the protocol fee multiplier.
                const receipt = await exchange
                    .setProtocolFeeMultiplier(protocolFeeMultiplier)
                    .awaitTransactionSuccessAsync();

                // Ensure that the correct events were logged.
                const logs = filterLogsToArguments<ExchangeProtocolFeeMultiplierEventArgs>(
                    receipt.logs,
                    ExchangeEvents.ProtocolFeeMultiplier,
                );
                expect(logs).to.be.deep.eq([
                    {
                        oldProtocolFeeMultiplier: constants.ZERO_AMOUNT,
                        updatedProtocolFeeMultiplier: protocolFeeMultiplier,
                    },
                ]);

                // Ensure that the protocol fee multiplier was set correctly.
                const multiplier = await exchange.protocolFeeMultiplier().callAsync();
                expect(multiplier).bignumber.to.be.eq(protocolFeeMultiplier);
            });
        });
    });

    describe('transferring ownership', () => {
        // Removes authorization of the "externally owned address" owner and transfers the authorization
        // to the asset proxy owner.
        async function transferAuthorizationAndAssertSuccessAsync(contract: Authorizable): Promise<void> {
            // Remove authorization from the old owner.
            let receipt = await contract.removeAuthorizedAddress(owner).awaitTransactionSuccessAsync({ from: owner });

            // Ensure that the correct log was recorded.
            let logs = filterLogsToArguments<AuthorizableAuthorizedAddressRemovedEventArgs>(
                receipt.logs,
                AuthorizableEvents.AuthorizedAddressRemoved,
            );
            expect(logs).to.be.deep.eq([{ target: owner, caller: owner }]);

            // Ensure that the owner was actually removed.
            let isAuthorized = await contract.authorized(owner).callAsync();
            expect(isAuthorized).to.be.false();

            // Authorize the asset-proxy owner.
            receipt = await contract.addAuthorizedAddress(governor.address).awaitTransactionSuccessAsync({
                from: owner,
            });

            // Ensure that the correct log was recorded.
            logs = filterLogsToArguments<AuthorizableAuthorizedAddressAddedEventArgs>(
                receipt.logs,
                AuthorizableEvents.AuthorizedAddressAdded,
            );
            expect(logs).to.be.deep.eq([{ target: governor.address, caller: owner }]);

            // Ensure that the asset-proxy owner was actually authorized.
            isAuthorized = await contract.authorized(governor.address).callAsync();
            expect(isAuthorized).to.be.true();
        }

        // Transfers ownership of a contract to the asset-proxy owner, and ensures that the change was actually made.
        async function transferOwnershipAndAssertSuccessAsync(contract: Ownable): Promise<void> {
            // Transfer ownership to the new owner.
            await contract.transferOwnership(governor.address).awaitTransactionSuccessAsync({ from: owner });

            // Ensure that the owner address has been updated.
            const ownerAddress = await contract.owner().callAsync();
            expect(ownerAddress).to.be.eq(governor.address);
        }

        it('should transfer authorization of the owner to the asset-proxy owner in the staking contracts', async () => {
            // Transfer authorization of the staking system. We intentionally neglect
            // to add the asset-proxy owner as an authorized address in the asset proxies
            // as a security precaution.
            await transferAuthorizationAndAssertSuccessAsync(stakingProxy);
        });

        it('should transfer ownership of all appropriate contracts to the asset-proxy owner', async () => {
            // Transfer ownership of most contracts (we exclude contracts that are not ownable).
            await transferOwnershipAndAssertSuccessAsync(exchange);
            await transferOwnershipAndAssertSuccessAsync(staking);
            await transferOwnershipAndAssertSuccessAsync(stakingProxy);
            await transferOwnershipAndAssertSuccessAsync(erc20Proxy);
            await transferOwnershipAndAssertSuccessAsync(erc721Proxy);
            await transferOwnershipAndAssertSuccessAsync(erc1155Proxy);
            await transferOwnershipAndAssertSuccessAsync(multiAssetProxy);
        });
    });
});
// tslint:enable:no-unnecessary-type-assertion
