import { expect } from "chai";
import hre, { deployments, waffle } from "hardhat";
import "@nomiclabs/hardhat-ethers";
import { deployContract, getMock, getMultiSend, getSafeWithOwners } from "../utils/setup";
import { buildContractCall, buildSafeTransaction, executeTx, MetaTransaction, safeApproveHash } from "../../src/utils/execution";
import { buildMultiSendSafeTx, encodeMultiSend } from "../../src/utils/multisend";
import { parseEther } from "@ethersproject/units";

describe("MultiSend", async () => {

    const [user1, user2] = waffle.provider.getWallets();

    const setupTests = deployments.createFixture(async ({ deployments }) => {
        await deployments.fixture();
        const setterSource = `
            contract StorageSetter {
                function setStorage(bytes3 data) public {
                    bytes32 slot = 0x4242424242424242424242424242424242424242424242424242424242424242;
                    // solhint-disable-next-line no-inline-assembly
                    assembly {
                        sstore(slot, data)
                    }
                }
            }`
        const storageSetter = await deployContract(user1, setterSource);
        return {
            safe: await getSafeWithOwners([user1.address]),
            multiSend: await getMultiSend(),
            mock: await getMock(),
            storageSetter
        }
    })

    describe("multiSend", async () => {

        it('should enforce delegatecall to MultiSend', async () => {
            const { multiSend } = await setupTests()
            const source = `
            contract Test {
                function killme() public {
                    selfdestruct(payable(msg.sender));
                }
            }`
            const killLib = await deployContract(user1, source);

            const nestedTransactionData = encodeMultiSend([ buildContractCall(killLib, "killme", [], 0)])

            let multiSendCode = await hre.ethers.provider.getCode(multiSend.address)
            await expect(
                multiSend.multiSend(nestedTransactionData)
            ).to.be.revertedWith("MultiSend should only be called via delegatecall")

            expect(await hre.ethers.provider.getCode(multiSend.address)).to.be.eq(multiSendCode)
        })

        it('Should fail when using invalid operation', async () => {
            const { safe, multiSend } = await setupTests()

            const txs = [buildSafeTransaction({to: user2.address, operation: 2, nonce: 0})]
            const safeTx = buildMultiSendSafeTx(multiSend, txs, await safe.nonce())
            await expect(
                executeTx(safe, safeTx, [ await safeApproveHash(user1, safe, safeTx, true) ])
            ).to.revertedWith("GS013")
        })

        it('Can execute empty multisend', async () => {
            const { safe, multiSend } = await setupTests()

            const txs: MetaTransaction[] = []
            const safeTx = buildMultiSendSafeTx(multiSend, txs, await safe.nonce())
            await expect(
                executeTx(safe, safeTx, [ await safeApproveHash(user1, safe, safeTx, true) ])
            ).to.emit(safe, "ExecutionSuccess")
        })

        it('Can execute single ether transfer', async () => {
            const { safe, multiSend } = await setupTests()
            await user1.sendTransaction({to: safe.address, value: parseEther("1")})
            const userBalance = await hre.ethers.provider.getBalance(user2.address)
            await expect(await hre.ethers.provider.getBalance(safe.address)).to.be.deep.eq(parseEther("1"))

            const txs: MetaTransaction[] = [buildSafeTransaction({to: user2.address, value: parseEther("1"), nonce: 0})]
            const safeTx = buildMultiSendSafeTx(multiSend, txs, await safe.nonce())
            await expect(
                executeTx(safe, safeTx, [ await safeApproveHash(user1, safe, safeTx, true) ])
            ).to.emit(safe, "ExecutionSuccess")

            await expect(await hre.ethers.provider.getBalance(safe.address)).to.be.deep.eq(parseEther("0"))
            await expect(await hre.ethers.provider.getBalance(user2.address)).to.be.deep.eq(userBalance.add(parseEther("1")))
        })

        it('reverts all tx if any fails', async () => {
            const { safe, multiSend } = await setupTests()
            await user1.sendTransaction({to: safe.address, value: parseEther("1")})
            const userBalance = await hre.ethers.provider.getBalance(user2.address)
            await expect(await hre.ethers.provider.getBalance(safe.address)).to.be.deep.eq(parseEther("1"))

            const txs: MetaTransaction[] = [
                buildSafeTransaction({to: user2.address, value: parseEther("1"), nonce: 0}),
                buildSafeTransaction({to: user2.address, value: parseEther("1"), nonce: 0}),
            ]
            const safeTx = buildMultiSendSafeTx(multiSend, txs, await safe.nonce(), { safeTxGas: 1 })
            await expect(
                executeTx(safe, safeTx, [ await safeApproveHash(user1, safe, safeTx, true) ])
            ).to.emit(safe, "ExecutionFailure")

            await expect(await hre.ethers.provider.getBalance(safe.address)).to.be.deep.eq(parseEther("1"))
            await expect(await hre.ethers.provider.getBalance(user2.address)).to.be.deep.eq(userBalance)
        })

        it('can be used when ETH is sent with execution', async () => {
            const { safe, multiSend, storageSetter } = await setupTests()

            const txs: MetaTransaction[] = [
                buildContractCall(storageSetter, "setStorage", ["0xbaddad"], 0)
            ]
            const safeTx = buildMultiSendSafeTx(multiSend, txs, await safe.nonce())
            
            await expect(await hre.ethers.provider.getBalance(safe.address)).to.be.deep.eq(parseEther("0"))

            await expect(
                executeTx(safe, safeTx, [ await safeApproveHash(user1, safe, safeTx, true) ], { value: parseEther("1") })
            ).to.emit(safe, "ExecutionSuccess")

            await expect(await hre.ethers.provider.getBalance(safe.address)).to.be.deep.eq(parseEther("1"))
        })

        it('can execute contract calls', async () => {
            const { safe, multiSend, storageSetter } = await setupTests()

            const txs: MetaTransaction[] = [
                buildContractCall(storageSetter, "setStorage", ["0xbaddad"], 0)
            ]
            const safeTx = buildMultiSendSafeTx(multiSend, txs, await safe.nonce())
            await expect(
                executeTx(safe, safeTx, [ await safeApproveHash(user1, safe, safeTx, true) ])
            ).to.emit(safe, "ExecutionSuccess")

            await expect(
                await hre.ethers.provider.getStorageAt(safe.address, "0x4242424242424242424242424242424242424242424242424242424242424242")
            ).to.be.eq("0x" + "".padEnd(64, "0"))
            await expect(
                await hre.ethers.provider.getStorageAt(storageSetter.address, "0x4242424242424242424242424242424242424242424242424242424242424242")
            ).to.be.eq("0x" + "baddad".padEnd(64, "0"))
        })

        it('can execute contract delegatecalls', async () => {
            const { safe, multiSend, storageSetter } = await setupTests()

            const txs: MetaTransaction[] = [
                buildContractCall(storageSetter, "setStorage", ["0xbaddad"], 0, true)
            ]
            const safeTx = buildMultiSendSafeTx(multiSend, txs, await safe.nonce())
            await expect(
                executeTx(safe, safeTx, [ await safeApproveHash(user1, safe, safeTx, true) ])
            ).to.emit(safe, "ExecutionSuccess")

            await expect(
                await hre.ethers.provider.getStorageAt(safe.address, "0x4242424242424242424242424242424242424242424242424242424242424242")
            ).to.be.eq("0x" + "baddad".padEnd(64, "0"))
            await expect(
                await hre.ethers.provider.getStorageAt(storageSetter.address, "0x4242424242424242424242424242424242424242424242424242424242424242")
            ).to.be.eq("0x" + "".padEnd(64, "0"))
        })

        it('can execute all calls in combination', async () => {
            const { safe, multiSend, storageSetter } = await setupTests()
            await user1.sendTransaction({to: safe.address, value: parseEther("1")})
            const userBalance = await hre.ethers.provider.getBalance(user2.address)
            await expect(await hre.ethers.provider.getBalance(safe.address)).to.be.deep.eq(parseEther("1"))

            const txs: MetaTransaction[] = [
                buildSafeTransaction({to: user2.address, value: parseEther("1"), nonce: 0}),
                buildContractCall(storageSetter, "setStorage", ["0xbaddad"], 0, true),
                buildContractCall(storageSetter, "setStorage", ["0xbaddad"], 0)
            ]
            const safeTx = buildMultiSendSafeTx(multiSend, txs, await safe.nonce())
            await expect(
                executeTx(safe, safeTx, [ await safeApproveHash(user1, safe, safeTx, true) ])
            ).to.emit(safe, "ExecutionSuccess")
            
            await expect(await hre.ethers.provider.getBalance(safe.address)).to.be.deep.eq(parseEther("0"))
            await expect(await hre.ethers.provider.getBalance(user2.address)).to.be.deep.eq(userBalance.add(parseEther("1")))
            await expect(
                await hre.ethers.provider.getStorageAt(safe.address, "0x4242424242424242424242424242424242424242424242424242424242424242")
            ).to.be.eq("0x" + "baddad".padEnd(64, "0"))
            await expect(
                await hre.ethers.provider.getStorageAt(storageSetter.address, "0x4242424242424242424242424242424242424242424242424242424242424242")
            ).to.be.eq("0x" + "baddad".padEnd(64, "0"))
        })
    })
})