import { expect } from "chai";
import hre, { deployments, waffle, ethers } from "hardhat";
import "@nomiclabs/hardhat-ethers";
import { compile, getCreateCall, getSafeWithOwners } from "../utils/setup";
import { buildContractCall, executeTx, safeApproveHash } from "../../src/utils/execution";
import { parseEther } from "@ethersproject/units";

const CONTRACT_SOURCE = `
contract Test {
    address public creator;
    constructor() payable {
        creator = msg.sender;
    }

    function x() public pure returns (uint) {
        return 21;
    }
}`

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

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

    const setupTests = deployments.createFixture(async ({ deployments }) => {
        await deployments.fixture();
        const testContract = await compile(CONTRACT_SOURCE);
        return {
            safe: await getSafeWithOwners([user1.address]),
            createCall: await getCreateCall(),
            testContract
        }
    })

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

        it('should revert if called directly and no value is on the factory', async () => {
            const { createCall, testContract } = await setupTests()
            await expect(
                createCall.performCreate(1, testContract.data)
            ).to.be.revertedWith("Could not deploy contract")
        })

        it('can call factory directly', async () => {
            const { createCall, testContract } = await setupTests()
            const createCallNonce = await ethers.provider.getTransactionCount(createCall.address)
            const address = ethers.utils.getContractAddress({ from: createCall.address, nonce: createCallNonce })

            await expect(
                createCall.performCreate(0, testContract.data)
            ).to.emit(createCall, "ContractCreation").withArgs(address)

            const newContract = new ethers.Contract(address, testContract.interface, user1)
            expect(await newContract.creator()).to.be.eq(createCall.address)
        })

        it('should fail if Safe does not have value to send along', async () => {
            const { safe, createCall, testContract } = await setupTests()

            const tx = await buildContractCall(createCall, "performCreate", [1, testContract.data], await safe.nonce(), true)
            await expect(
                executeTx(safe, tx, [await safeApproveHash(user1, safe, tx, true)])
            ).to.revertedWith("GS013")
        })

        it('should successfully create contract and emit event', async () => {
            const { safe, createCall, testContract } = await setupTests()

            const safeEthereumNonce = await ethers.provider.getTransactionCount(safe.address)
            const address = ethers.utils.getContractAddress({ from: safe.address, nonce: safeEthereumNonce })

            // We require this as 'emit' check the address of the event
            const safeCreateCall = createCall.attach(safe.address)
            const tx = await buildContractCall(createCall, "performCreate", [0, testContract.data], await safe.nonce(), true)
            await expect(
                executeTx(safe, tx, [await safeApproveHash(user1, safe, tx, true)])
            ).to.emit(safe, "ExecutionSuccess").and.to.emit(safeCreateCall, "ContractCreation").withArgs(address)

            const newContract = new ethers.Contract(address, testContract.interface, user1)
            expect(await newContract.creator()).to.be.eq(safe.address)
        })

        it('should successfully create contract and send along ether', async () => {
            const { safe, createCall, testContract } = await setupTests()
            await user1.sendTransaction({ to: safe.address, value: parseEther("1") })
            await expect(await hre.ethers.provider.getBalance(safe.address)).to.be.deep.eq(parseEther("1"))

            const safeEthereumNonce = await ethers.provider.getTransactionCount(safe.address)
            const address = ethers.utils.getContractAddress({ from: safe.address, nonce: safeEthereumNonce })

            // We require this as 'emit' check the address of the event
            const safeCreateCall = createCall.attach(safe.address)
            const tx = await buildContractCall(createCall, "performCreate", [parseEther("1"), testContract.data], await safe.nonce(), true)
            await expect(
                executeTx(safe, tx, [await safeApproveHash(user1, safe, tx, true)])
            ).to.emit(safe, "ExecutionSuccess").and.to.emit(safeCreateCall, "ContractCreation").withArgs(address)

            await expect(await hre.ethers.provider.getBalance(safe.address)).to.be.deep.eq(parseEther("0"))
            await expect(await hre.ethers.provider.getBalance(address)).to.be.deep.eq(parseEther("1"))
            const newContract = new ethers.Contract(address, testContract.interface, user1)
            expect(await newContract.creator()).to.be.eq(safe.address)
        })
    })

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

        const salt = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("createCall"))

        it('should revert if called directly and no value is on the factory', async () => {
            const { createCall, testContract } = await setupTests()
            await expect(
                createCall.performCreate2(1, testContract.data, salt)
            ).to.be.revertedWith("Could not deploy contract")
        })

        it('can call factory directly', async () => {
            const { createCall, testContract } = await setupTests()
            const address = ethers.utils.getCreate2Address(createCall.address, salt, ethers.utils.keccak256(testContract.data))

            await expect(
                createCall.performCreate2(0, testContract.data, salt)
            ).to.emit(createCall, "ContractCreation").withArgs(address)

            const newContract = new ethers.Contract(address, testContract.interface, user1)
            expect(await newContract.creator()).to.be.eq(createCall.address)
        })

        it('should fail if Safe does not have value to send along', async () => {
            const { safe, createCall, testContract } = await setupTests()

            const tx = await buildContractCall(createCall, "performCreate2", [1, testContract.data, salt], await safe.nonce(), true)
            await expect(
                executeTx(safe, tx, [await safeApproveHash(user1, safe, tx, true)])
            ).to.revertedWith("GS013")
        })

        it('should successfully create contract and emit event', async () => {
            const { safe, createCall, testContract } = await setupTests()

            const address = ethers.utils.getCreate2Address(safe.address, salt, ethers.utils.keccak256(testContract.data))

            // We require this as 'emit' check the address of the event
            const safeCreateCall = createCall.attach(safe.address)
            const tx = await buildContractCall(createCall, "performCreate2", [0, testContract.data, salt], await safe.nonce(), true)
            await expect(
                executeTx(safe, tx, [await safeApproveHash(user1, safe, tx, true)])
            ).to.emit(safe, "ExecutionSuccess").and.to.emit(safeCreateCall, "ContractCreation").withArgs(address)

            const newContract = new ethers.Contract(address, testContract.interface, user1)
            expect(await newContract.creator()).to.be.eq(safe.address)
        })

        it('should successfully create contract and send along ether', async () => {
            const { safe, createCall, testContract } = await setupTests()
            await user1.sendTransaction({ to: safe.address, value: parseEther("1") })
            await expect(await hre.ethers.provider.getBalance(safe.address)).to.be.deep.eq(parseEther("1"))

            const address = ethers.utils.getCreate2Address(safe.address, salt, ethers.utils.keccak256(testContract.data))

            // We require this as 'emit' check the address of the event
            const safeCreateCall = createCall.attach(safe.address)
            const tx = await buildContractCall(createCall, "performCreate2", [parseEther("1"), testContract.data, salt], await safe.nonce(), true)
            await expect(
                executeTx(safe, tx, [await safeApproveHash(user1, safe, tx, true)])
            ).to.emit(safe, "ExecutionSuccess").and.to.emit(safeCreateCall, "ContractCreation").withArgs(address)

            await expect(await hre.ethers.provider.getBalance(safe.address)).to.be.deep.eq(parseEther("0"))
            await expect(await hre.ethers.provider.getBalance(address)).to.be.deep.eq(parseEther("1"))
            const newContract = new ethers.Contract(address, testContract.interface, user1)
            expect(await newContract.creator()).to.be.eq(safe.address)
        })
    })
})