import { ethers, network, waffle } from 'hardhat';
import { swapContractFixtureInFork } from './shared/fixtures';
import { Wallet } from '@ethersproject/wallet';
import { RubicRouterV2, TestERC20, TestMessages, WETH9 } from '../typechain';
import { expect } from 'chai';
import {
    DEADLINE,
    DST_CHAIN_ID,
    DEFAULT_AMOUNT_IN,
    VERSION_V2,
    DEFAULT_AMOUNT_OUT_MIN,
    EXECUTOR_ADDRESS,
    INTEGRATOR,
    DEFAULT_AMOUNT_IN_USDC,
    MESSAGE_BUS_FEE
} from './shared/consts';
import { BigNumber as BN, BigNumberish, ContractTransaction } from 'ethers';
import { getRouterV2 } from './shared/utils';
import { calcCryptoFees, calcTokenFees } from 'rubic-bridge-base/lib';
const hre = require('hardhat');

const createFixtureLoader = waffle.createFixtureLoader;

const envConfig = require('dotenv').config();
const {
    ROUTERS_POLYGON: TEST_ROUTERS,
    NATIVE_POLYGON: TEST_NATIVE,
    BUS_POLYGON_MAIN: TEST_BUS
} = envConfig.parsed || {};

describe('RubicCrossChainV2', () => {
    let wallet: Wallet, other: Wallet;
    let swapToken: TestERC20;
    let transitToken: TestERC20;
    let swapMain: RubicRouterV2;
    let router: string;
    let wnative: WETH9;

    let testMessagesContract: TestMessages;

    let loadFixture: ReturnType<typeof createFixtureLoader>;

    async function callTransferWithSwapV2Native(
        amountOutMinimum: BigNumberish,
        {
            receiver = null,
            amountIn = DEFAULT_AMOUNT_IN,
            dstChainID = DST_CHAIN_ID,
            srcDEX = router,
            srcPath = [wnative.address, transitToken.address],
            nativeIn = null,
            integrator = INTEGRATOR,
            nativeOut = true
        } = {}
    ): Promise<ContractTransaction> {
        const { totalCryptoFee } = await calcCryptoFees({
            bridge: swapMain,
            integrator,
            dstChainID
        });

        return swapMain.transferWithSwapV2Native(
            receiver === null ? wallet.address : receiver,
            amountIn,
            dstChainID,
            {
                dex: srcDEX,
                path: srcPath,
                deadline: DEADLINE,
                amountOutMinimum
            },
            {
                dex: router,
                nativeOut: nativeOut,
                receiverEOA: other.address,
                integrator: integrator,
                version: VERSION_V2,
                path: [wnative.address, transitToken.address],
                pathV3: '0x',
                deadline: DEADLINE,
                amountOutMinimum: DEFAULT_AMOUNT_OUT_MIN
            },
            '10000',
            {
                value:
                    nativeIn === null ? amountIn.add(totalCryptoFee).add(MESSAGE_BUS_FEE) : nativeIn
            }
        );
    }

    async function callTransferWithSwapV2(
        amountOutMinimum: BigNumberish,
        {
            receiver = null,
            amountIn = DEFAULT_AMOUNT_IN,
            dstChainID = DST_CHAIN_ID,
            srcDEX = router,
            srcPath = [wnative.address, transitToken.address],
            nativeIn = null,
            integrator = INTEGRATOR,
            nativeOut = true
        } = {}
    ): Promise<ContractTransaction> {
        const { totalCryptoFee } = await calcCryptoFees({
            bridge: swapMain,
            integrator,
            dstChainID
        });

        return swapMain.transferWithSwapV2(
            receiver === null ? wallet.address : receiver,
            amountIn,
            dstChainID,
            {
                dex: srcDEX,
                path: srcPath,
                deadline: DEADLINE,
                amountOutMinimum
            },
            {
                dex: router,
                nativeOut: nativeOut,
                receiverEOA: other.address,
                integrator: integrator,
                version: VERSION_V2,
                path: [wnative.address, transitToken.address],
                pathV3: '0x',
                deadline: DEADLINE,
                amountOutMinimum: DEFAULT_AMOUNT_OUT_MIN
            },
            '10000',
            { value: nativeIn === null ? totalCryptoFee.add(MESSAGE_BUS_FEE) : nativeIn }
        );
    }

    async function getAmountOutMin(
        amountIn = DEFAULT_AMOUNT_IN,
        path = [wnative.address, transitToken.address]
    ) {
        const routerV2 = await getRouterV2(wallet, router);

        return (await routerV2.getAmountsOut(amountIn, path))[1];
    }

    // async function getAmountIn(
    //     amountOut = DEFAULT_AMOUNT_OUT_MIN,
    //     path = [transitToken.address, swapToken.address]
    // ) {
    //     const routerV2 = await getRouterV2(wallet, router);
    //     return (await routerV2.getAmountsIn(amountOut, path))[0];
    // }

    async function getMessage(
        messagesContract: TestMessages,
        _nonce: BigNumberish,
        dstChainId: BigNumberish,
        {
            dex = router,
            receiverEOA = other.address,
            integrator = ethers.constants.AddressZero,
            version = VERSION_V2,
            path = [wnative.address, transitToken.address],
            pathV3 = '0x',
            deadline = DEADLINE,
            amountOutMinimum = DEFAULT_AMOUNT_OUT_MIN,
            _receiver = wallet.address,
            nativeOut = true
        } = {}
    ): Promise<string> {
        return messagesContract.getMessage(
            {
                dex,
                nativeOut,
                receiverEOA,
                integrator,
                version,
                path,
                pathV3,
                deadline,
                amountOutMinimum
            },
            _nonce,
            dstChainId
        );
    }

    // async function getID(
    //     messagesContract: TestMessages,
    //     _nonce: BigNumberish,
    //     {
    //         dex = router,
    //         receiverEOA = other.address,
    //         integrator = INTEGRATOR,
    //         version = VERSION_V2,
    //         path = [wnative.address, transitToken.address],
    //         pathV3 = '0x',
    //         deadline = DEADLINE,
    //         amountOutMinimum = DEFAULT_AMOUNT_OUT_MIN,
    //         _receiver = wallet.address,
    //         nativeOut = true,
    //         _srcChainId = chainId,
    //         _dstChainId = DST_CHAIN_ID
    //     } = {}
    // ): Promise<string> {
    //     return messagesContract.getID(
    //         _srcChainId,
    //         _dstChainId,
    //         {
    //             dex,
    //             nativeOut,
    //             receiverEOA,
    //             integrator,
    //             version,
    //             path,
    //             pathV3,
    //             deadline,
    //             amountOutMinimum
    //         },
    //         _nonce
    //     );
    // }

    before('create fixture loader', async () => {
        [wallet, other] = await (ethers as any).getSigners();
        loadFixture = createFixtureLoader([wallet, other]);
    });

    beforeEach('deploy fixture', async () => {
        ({ swapMain, swapToken, transitToken, wnative, router, testMessagesContract } =
            await loadFixture(swapContractFixtureInFork));
    });

    it('constructor initializes', async () => {
        expect(await swapMain.nativeWrap()).to.eq(TEST_NATIVE);
        expect(await swapMain.messageBus()).to.eq(TEST_BUS);

        const routers = TEST_ROUTERS.split(',');
        expect(await swapMain.getAvailableRouters()).to.deep.eq(routers);
    });

    describe('#WithSwapTests', () => {
        describe('#transferWithSwapV2Native', () => {
            it('Should swap native to token and transfer through Celer', async () => {
                await swapMain.setMaxTokenAmount(
                    transitToken.address,
                    ethers.utils.parseEther('1000')
                );

                const amountOutMin = await getAmountOutMin(
                    ethers.BigNumber.from('20000000000000000000')
                );
                const _amountIn = ethers.BigNumber.from('20000000000000000000');

                await expect(
                    callTransferWithSwapV2Native(amountOutMin, {
                        amountIn: _amountIn,
                        srcPath: [wnative.address, transitToken.address]
                    })
                ).to.emit(swapMain, 'CrossChainRequestSent');
            });
            it('Should swap native to token and fail transfer through Celer', async () => {
                await swapMain.setMaxTokenAmount(
                    transitToken.address,
                    ethers.utils.parseEther('1000')
                );

                const amountOutMin = await getAmountOutMin();

                await expect(
                    callTransferWithSwapV2Native(amountOutMin, {
                        srcPath: [wnative.address, transitToken.address]
                    })
                ).to.be.revertedWith('amount too small');
            });
        });
        describe('#transferWithSwapV2', () => {
            it('Should swap token to transitToken and transfer through Сeler', async () => {
                await swapToken.approve(swapMain.address, ethers.constants.MaxUint256);
                await swapMain.setMaxTokenAmount(
                    transitToken.address,
                    ethers.utils.parseEther('1000')
                );

                const amountOutMin = await getAmountOutMin(DEFAULT_AMOUNT_IN, [
                    swapToken.address,
                    transitToken.address
                ]);

                //const ID = await getID(testMessagesContract, (await swapMain.nonce()).add('1'));
                await expect(
                    callTransferWithSwapV2(amountOutMin, {
                        srcPath: [swapToken.address, transitToken.address]
                    })
                ).to.emit(swapMain, 'CrossChainRequestSent');
                //.withArgs(ID, DST_CHAIN_ID, DEFAULT_AMOUNT_IN, swapToken.address);
            });

            it('Should swap token to native and transfer through Сeler', async () => {
                await swapToken.approve(swapMain.address, ethers.constants.MaxUint256);
                await swapMain.setMaxTokenAmount(wnative.address, ethers.utils.parseEther('10000'));

                // amountIn is 100$
                const amountOutMin = await getAmountOutMin(DEFAULT_AMOUNT_IN_USDC, [
                    swapToken.address,
                    wnative.address
                ]);

                //const ID = await getID(testMessagesContract, (await swapMain.nonce()).add('1'));

                await expect(
                    callTransferWithSwapV2(amountOutMin, {
                        srcPath: [swapToken.address, wnative.address]
                    })
                ).to.emit(swapMain, 'CrossChainRequestSent');
                //.withArgs(ID, DST_CHAIN_ID, DEFAULT_AMOUNT_IN, swapToken.address);
            });
        });
        describe('#executeMessageWithTransfer', () => {
            beforeEach('setup for target executions', async () => {
                // transfer 1000 USDC
                await transitToken.transfer(swapMain.address, ethers.BigNumber.from('1000000000'));
            });
            describe('target swap should emit correct event', async () => {
                let nonce: BN;
                let message: string;

                beforeEach('setup before swap', async () => {
                    nonce = (await swapMain.nonce()).add('1');

                    message = await getMessage(testMessagesContract, nonce, DST_CHAIN_ID, {
                        path: [transitToken.address, swapToken.address],
                        amountOutMinimum: ethers.BigNumber.from('200000000000000000') // 0.2 eth for 1000$ is min
                    });
                });
                it('should successfully swap V2 with rubic fee', async () => {
                    await hre.network.provider.request({
                        method: 'hardhat_impersonateAccount',
                        params: [TEST_BUS]
                    });

                    const bus = await ethers.getSigner(TEST_BUS);

                    await network.provider.send('hardhat_setBalance', [
                        bus.address,
                        '0x152D02C7E14AF6800000' // 100000 eth
                    ]);

                    const _swapMain = swapMain.connect(bus);

                    //let tokenBalanceBefore = await transitToken.balanceOf(swapMain.address);
                    await expect(
                        _swapMain.executeMessageWithTransfer(
                            ethers.constants.AddressZero,
                            transitToken.address,
                            ethers.BigNumber.from('1000000000'),
                            DST_CHAIN_ID,
                            message,
                            EXECUTOR_ADDRESS
                        )
                    ).to.emit(swapMain, 'CrossChainProcessed');
                    let tokenBalanceAfter = await transitToken.balanceOf(swapMain.address);

                    const { feeAmount } = await calcTokenFees({
                        bridge: swapMain,
                        amountWithFee: ethers.BigNumber.from('1000000000')
                    });
                    // take only platform comission in transit token
                    await expect(Number(tokenBalanceAfter)).to.be.eq(feeAmount);
                });

                it('should fail swap V2 with rubic fee and transfer tokens', async () => {
                    await hre.network.provider.request({
                        method: 'hardhat_impersonateAccount',
                        params: [TEST_BUS]
                    });

                    const bus = await ethers.getSigner(TEST_BUS);

                    await network.provider.send('hardhat_setBalance', [
                        bus.address,
                        '0x152D02C7E14AF6800000' // 100000 eth
                    ]);

                    const _swapMain = swapMain.connect(bus);

                    message = await getMessage(testMessagesContract, nonce, DST_CHAIN_ID, {
                        path: [transitToken.address, swapToken.address],
                        amountOutMinimum: ethers.BigNumber.from('2000000000000000000') // 2 eth for 1000$ is minOut, too much
                    });

                    //const tokenBalanceBefore = await transitToken.balanceOf(swapMain.address);
                    await expect(
                        _swapMain.executeMessageWithTransfer(
                            ethers.constants.AddressZero,
                            transitToken.address,
                            ethers.BigNumber.from('1000000000'),
                            DST_CHAIN_ID,
                            message,
                            EXECUTOR_ADDRESS
                        )
                    ).to.emit(swapMain, 'CrossChainProcessed');

                    const tokenBalanceAfter = await transitToken.balanceOf(swapMain.address);

                    const { RubicFee, feeAmount } = await calcTokenFees({
                        bridge: swapMain,
                        amountWithFee: ethers.BigNumber.from('1000000000')
                        //integrator: INTEGRATOR,
                    });

                    // take only platform comission in transit token
                    await expect(tokenBalanceAfter).to.be.eq(feeAmount);

                    const collectedFee1 = await swapMain.availableRubicTokenFee(
                        transitToken.address
                    );

                    await expect(collectedFee1).to.be.eq(RubicFee);

                    const integratorCollectedFee1 = await swapMain.availableIntegratorTokenFee(
                        transitToken.address,
                        INTEGRATOR
                    );
                    await expect(Number(integratorCollectedFee1)).to.be.eq(0);
                });

                describe('target swap should take integrator & rubic fee', async () => {
                    beforeEach('set integrator and rubic fee', async () => {
                        await swapMain.setIntegratorInfo(INTEGRATOR, {
                            isIntegrator: true,
                            tokenFee: '3000',
                            RubicTokenShare: '400000',
                            RubicFixedCryptoShare: '800000',
                            fixedFeeAmount: ethers.utils.parseEther('2')
                        }); // 0.3 %

                        message = await getMessage(testMessagesContract, nonce, DST_CHAIN_ID, {
                            path: [transitToken.address, swapToken.address],
                            integrator: INTEGRATOR,
                            amountOutMinimum: ethers.BigNumber.from('200000000000000000') // 0.2 eth for 1000$ is minOut, too much
                        });
                    });

                    it('should successfully swapV2 token to token with rubic & integrator fee', async () => {
                        await hre.network.provider.request({
                            method: 'hardhat_impersonateAccount',
                            params: [TEST_BUS]
                        });

                        const bus = await ethers.getSigner(TEST_BUS);

                        await network.provider.send('hardhat_setBalance', [
                            bus.address,
                            '0x152D02C7E14AF6800000' // 100000 eth
                        ]);

                        const _swapMain = swapMain.connect(bus);

                        //let tokenBalanceBefore = await transitToken.balanceOf(swapMain.address);
                        await expect(
                            _swapMain.executeMessageWithTransfer(
                                ethers.constants.AddressZero,
                                transitToken.address,
                                ethers.BigNumber.from('1000000000'),
                                DST_CHAIN_ID,
                                message,
                                EXECUTOR_ADDRESS
                            )
                        ).to.emit(swapMain, 'CrossChainProcessed');
                        const tokenBalanceAfter = await transitToken.balanceOf(swapMain.address);
                        const collectedFee1 = await swapMain.availableRubicTokenFee(
                            transitToken.address
                        );

                        const integratorCollectedFee1 = await swapMain.availableIntegratorTokenFee(
                            transitToken.address,
                            INTEGRATOR
                        );

                        const { integratorFee, RubicFee, feeAmount } = await calcTokenFees({
                            bridge: swapMain,
                            amountWithFee: ethers.BigNumber.from('1000000000'),
                            integrator: INTEGRATOR
                        });

                        await expect(integratorCollectedFee1).to.be.eq(integratorFee);

                        // take platform comission in transit token
                        await expect(collectedFee1).to.be.eq(RubicFee);

                        await expect(tokenBalanceAfter).to.be.eq(feeAmount);
                    });

                    it('should successfully swapV2 token to native with rubic & integrator fee', async () => {
                        await hre.network.provider.request({
                            method: 'hardhat_impersonateAccount',
                            params: [TEST_BUS]
                        });

                        const bus = await ethers.getSigner(TEST_BUS);

                        await network.provider.send('hardhat_setBalance', [
                            bus.address,
                            '0x152D02C7E14AF6800000' // 100000 eth
                        ]);

                        const _swapMain = swapMain.connect(bus);

                        //let tokenBalanceBefore = await transitToken.balanceOf(swapMain.address);

                        message = await getMessage(testMessagesContract, nonce, DST_CHAIN_ID, {
                            path: [transitToken.address, wnative.address],
                            integrator: INTEGRATOR,
                            amountOutMinimum: ethers.BigNumber.from('20000000000000000') // 0.02 eth for 1000$ is minOut
                        });

                        await expect(
                            _swapMain.executeMessageWithTransfer(
                                ethers.constants.AddressZero,
                                transitToken.address,
                                ethers.BigNumber.from('1000000000'),
                                DST_CHAIN_ID,
                                message,
                                EXECUTOR_ADDRESS
                            )
                        ).to.emit(swapMain, 'CrossChainProcessed');

                        const tokenBalanceAfter = await transitToken.balanceOf(swapMain.address);
                        const collectedFee1 = await swapMain.availableRubicTokenFee(
                            transitToken.address
                        );

                        const integratorCollectedFee1 = await swapMain.availableIntegratorTokenFee(
                            transitToken.address,
                            INTEGRATOR
                        );

                        const { integratorFee, RubicFee, feeAmount } = await calcTokenFees({
                            bridge: swapMain,
                            amountWithFee: ethers.BigNumber.from('1000000000'),
                            integrator: INTEGRATOR
                        });

                        await expect(integratorCollectedFee1).to.be.eq(integratorFee);

                        // take platform comission in transit token
                        await expect(collectedFee1).to.be.eq(RubicFee);

                        await expect(tokenBalanceAfter).to.be.eq(feeAmount);
                    });

                    it('should fail swap V2 with rubic & integrator fee', async () => {
                        await hre.network.provider.request({
                            method: 'hardhat_impersonateAccount',
                            params: [TEST_BUS]
                        });

                        const bus = await ethers.getSigner(TEST_BUS);

                        await network.provider.send('hardhat_setBalance', [
                            bus.address,
                            '0x152D02C7E14AF6800000' // 100000 eth
                        ]);

                        const _swapMain = swapMain.connect(bus);

                        //let tokenBalanceBefore = await transitToken.balanceOf(swapMain.address);

                        message = await getMessage(testMessagesContract, nonce, DST_CHAIN_ID, {
                            path: [transitToken.address, swapToken.address],
                            integrator: INTEGRATOR,
                            amountOutMinimum: ethers.BigNumber.from('20000000000000000000') // 20 eth for 1000$ is min out
                        });
                        await expect(
                            _swapMain.executeMessageWithTransfer(
                                ethers.constants.AddressZero,
                                transitToken.address,
                                ethers.BigNumber.from('1000000000'),
                                DST_CHAIN_ID,
                                message,
                                EXECUTOR_ADDRESS
                            )
                        ).to.emit(swapMain, 'CrossChainProcessed');
                        const tokenBalanceAfter = await transitToken.balanceOf(swapMain.address);
                        const collectedFee1 = await swapMain.availableRubicTokenFee(
                            transitToken.address
                        );

                        const integratorCollectedFee1 = await swapMain.availableIntegratorTokenFee(
                            transitToken.address,
                            INTEGRATOR
                        );

                        const { integratorFee, RubicFee, feeAmount } = await calcTokenFees({
                            bridge: swapMain,
                            amountWithFee: ethers.BigNumber.from('1000000000'),
                            integrator: INTEGRATOR
                        });

                        await expect(integratorCollectedFee1).to.be.eq(integratorFee);

                        // take platform comission in transit token
                        await expect(collectedFee1).to.be.eq(RubicFee);

                        await expect(tokenBalanceAfter).to.be.eq(feeAmount);
                    });
                });
            });
        });
    });
});
