import { Chain } from '__tests__/utils/chain';
import { mockInjector } from '__tests__/utils/mock-injector';
import { TOKENS as ALL_TOKENS } from '__tests__/utils/tokens';
import { Injector } from 'src/core/injector/injector';
import BigNumber from 'bignumber.js';
import { Web3Public } from 'src/core';
import { PriceTokenAmount } from 'src/common/tokens/price-token-amount';
import { PriceToken } from 'src/common/tokens/price-token';
import { TransactionReceipt } from 'web3-eth';
import { UniSwapV2EthereumProvider } from '@rsdk-features/instant-trades/dexes/ethereum/uni-swap-v2-ethereum/uni-swap-v2-ethereum-provider';
import { BLOCKCHAIN_NAME } from 'src/core/blockchain/models/blockchain-name';
import fn = jest.fn;

const TOKENS = ALL_TOKENS[BLOCKCHAIN_NAME.ETHEREUM];

export const uniswapV2TradeSpec = () =>
    describe('Uniswap V2 trade tests.', () => {
        let chain: Chain;
        let uniswapV2Provider: UniSwapV2EthereumProvider;
        let web3Public: Web3Public;
        let userAddress: string;

        const getTransactionFeeByReceipt = async (
            transactionReceipt: TransactionReceipt
        ): Promise<BigNumber> => {
            const transaction = (await web3Public.getTransactionByHash(
                transactionReceipt.transactionHash
            ))!;
            return new BigNumber(transactionReceipt.gasUsed).multipliedBy(transaction.gasPrice);
        };

        const getTransactionFeeByHash = async (transactionHash: string): Promise<BigNumber> => {
            const transaction = (await web3Public.getTransactionByHash(transactionHash))!;
            const transactionReceipt = (await chain.web3.eth.getTransactionReceipt(
                transactionHash
            ))!;
            return new BigNumber(transactionReceipt.gasUsed).multipliedBy(transaction.gasPrice);
        };

        beforeAll(async () => {
            uniswapV2Provider = new UniSwapV2EthereumProvider();
        });

        beforeEach(async () => {
            chain = await Chain.reset(BLOCKCHAIN_NAME.ETHEREUM);
            const configuration = await chain.getConfiguration();
            await mockInjector(configuration);
            web3Public = Injector.web3PublicService.getWeb3Public(BLOCKCHAIN_NAME.ETHEREUM);
            userAddress = Injector.web3PrivateService.address;
        }, 20_000);

        test('Swap method must works with NATIVE-ERC20 trade', async () => {
            const ethTokenAmountToSwap = 1;
            const expectedToTokensAmount = 3173.460947; // constant data about tokens rate in 13961175 block
            const ethBalanceBefore = await web3Public.getBalance(userAddress);
            const usdtBalanceBefore = await web3Public.getBalance(userAddress, TOKENS.USDT.address);
            const from = await PriceTokenAmount.createFromToken({
                ...TOKENS.ETH,
                tokenAmount: new BigNumber(ethTokenAmountToSwap)
            });
            const to = await PriceToken.createFromToken(TOKENS.USDT);

            const trade = await uniswapV2Provider.calculate(from, to, {
                gasCalculation: 'disabled'
            });
            const transactionReceipt = await trade.swap();
            const ethBalanceAfter = await web3Public.getBalance(userAddress);
            const usdtBalanceAfter = await web3Public.getBalance(userAddress, TOKENS.USDT.address);
            const transactionFee = await getTransactionFeeByReceipt(transactionReceipt);

            expect(transactionReceipt.status).toBeTruthy();
            expect(
                ethBalanceAfter.isEqualTo(
                    ethBalanceBefore
                        .minus(ethTokenAmountToSwap * 10 ** TOKENS.ETH.decimals)
                        .minus(transactionFee)
                )
            ).toBeTruthy();
            expect(
                usdtBalanceAfter.isEqualTo(
                    usdtBalanceBefore.plus(expectedToTokensAmount * 10 ** TOKENS.USDT.decimals)
                )
            ).toBeTruthy();

            const trade1 = await uniswapV2Provider.calculate(from, to, {
                gasCalculation: 'disabled'
            });
            expect(trade1.to.weiAmount.isEqualTo(trade.to.weiAmount)).not.toBeTruthy();
        }, 400_000);

        test('Swap method must works with ERC20-NATIVE trade', async () => {
            const rbcTokenAmountToSwap = 1;
            const expectedToTokensAmount = '0.000068319082745321'; // constant data about tokens rate in 13961175 block
            const from = await PriceTokenAmount.createFromToken({
                ...TOKENS.RBC,
                tokenAmount: new BigNumber(rbcTokenAmountToSwap)
            });
            const to = await PriceToken.createFromToken(TOKENS.ETH);
            await chain.increaseTokensBalance(from, rbcTokenAmountToSwap, { inEtherUnits: true });
            const rbcBalanceBefore = await web3Public.getBalance(userAddress, from.address);
            const ethBalanceBefore = await web3Public.getBalance(userAddress);
            let approveTxHash = '';

            const onApprove = fn(hash => (approveTxHash = hash));
            const trade = await uniswapV2Provider.calculate(from, to, {
                gasCalculation: 'disabled'
            });
            const transactionReceipt = await trade.swap({ onApprove });
            const rbcBalanceAfter = await web3Public.getBalance(userAddress, from.address);
            const ethBalanceAfter = await web3Public.getBalance(userAddress);
            const approveTransactionFee = await getTransactionFeeByHash(approveTxHash);
            const transactionFee = await getTransactionFeeByReceipt(transactionReceipt);

            expect(transactionReceipt.status).toBeTruthy();
            expect(
                rbcBalanceAfter.isEqualTo(
                    rbcBalanceBefore.minus(rbcTokenAmountToSwap * 10 ** from.decimals)
                )
            ).toBeTruthy();

            expect(
                ethBalanceAfter.isEqualTo(
                    ethBalanceBefore
                        .plus(new BigNumber(expectedToTokensAmount).multipliedBy(10 ** to.decimals))
                        .minus(approveTransactionFee)
                        .minus(transactionFee)
                )
            ).toBeTruthy();
            expect(onApprove.mock.calls.length).toBe(1);
        }, 400_000);

        test('Swap method must works with ERC20-ERC20 trade', async () => {
            const rbcTokenAmountToSwap = 1;
            const expectedToTokensAmount = '0.216816'; // constant data about tokens rate in 13961175 block
            const from = await PriceTokenAmount.createFromToken({
                ...TOKENS.RBC,
                tokenAmount: new BigNumber(rbcTokenAmountToSwap)
            });
            const to = await PriceToken.createFromToken(TOKENS.USDT);
            await chain.increaseTokensBalance(from, rbcTokenAmountToSwap, { inEtherUnits: true });
            const rbcBalanceBefore = await web3Public.getBalance(userAddress, from.address);
            const usdtBalanceBefore = await web3Public.getBalance(userAddress, to.address);

            const trade = await uniswapV2Provider.calculate(from, to, {
                gasCalculation: 'disabled'
            });
            const transactionReceipt = await trade.swap();
            const rbcBalanceAfter = await web3Public.getBalance(userAddress, from.address);
            const usdtBalanceAfter = await web3Public.getBalance(userAddress, to.address);

            expect(transactionReceipt.status).toBeTruthy();
            expect(
                rbcBalanceAfter.isEqualTo(
                    rbcBalanceBefore.minus(rbcTokenAmountToSwap * 10 ** from.decimals)
                )
            ).toBeTruthy();

            expect(
                usdtBalanceAfter.isEqualTo(
                    usdtBalanceBefore.plus(
                        new BigNumber(expectedToTokensAmount).multipliedBy(10 ** to.decimals)
                    )
                )
            ).toBeTruthy();
        }, 400_000);
    });
