import { expect } from "chai";
import hre, { deployments, waffle, ethers } from "hardhat";
import "@nomiclabs/hardhat-ethers";
import { AddressZero } from "@ethersproject/constants";
import { compatFallbackHandlerContract, getCompatFallbackHandler, getSafeWithOwners } from "../utils/setup";
import { buildSignatureBytes, executeContractCallWithSigners, calculateSafeMessageHash, EIP712_SAFE_MESSAGE_TYPE, signHash } from "../../src/utils/execution";
import { chainId } from "../utils/encoding";
import { BigNumber } from "ethers";
import { killLibContract } from "../utils/contracts";

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

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

    const setupTests = deployments.createFixture(async ({ deployments }) => {
        await deployments.fixture();
        const signLib = await (await hre.ethers.getContractFactory("SignMessageLib")).deploy();
        const handler = await getCompatFallbackHandler()
        const safe = await getSafeWithOwners([user1.address, user2.address], 2, handler.address)
        const validator = (await compatFallbackHandlerContract()).attach(safe.address)
        const killLib = await killLibContract(user1);
        return {
            safe,
            validator,
            handler,
            killLib,
            signLib
        }
    })

    describe("ERC1155", async () => {
        it('to handle onERC1155Received', async () => {
            const { handler } = await setupTests()
            await expect(
                await handler.callStatic.onERC1155Received(AddressZero, AddressZero, 0, 0, "0x")
            ).to.be.eq("0xf23a6e61")
        })

        it('to handle onERC1155BatchReceived', async () => {
            const { handler } = await setupTests()
            await expect(
                await handler.callStatic.onERC1155BatchReceived(AddressZero, AddressZero, [], [], "0x")
            ).to.be.eq("0xbc197c81")
        })
    })

    describe("ERC721", async () => {
        it('to handle onERC721Received', async () => {
            const { handler } = await setupTests()
            await expect(
                await handler.callStatic.onERC721Received(AddressZero, AddressZero, 0, "0x")
            ).to.be.eq("0x150b7a02")
        })
    })

    describe("ERC777", async () => {
        it('to handle tokensReceived', async () => {
            const { handler } = await setupTests()
            await handler.callStatic.tokensReceived(AddressZero, AddressZero, AddressZero, 0, "0x", "0x")
        })
    })
    
    describe("isValidSignature(bytes,bytes)", async () => {

        it('should revert if called directly', async () => {
            const { handler } = await setupTests()
            await expect(handler.callStatic['isValidSignature(bytes,bytes)']("0xbaddad", "0x")).to.be.revertedWith("function call to a non-contract account")
        })

        it('should revert if message was not signed', async () => {
            const { validator } = await setupTests()
            await expect(validator.callStatic['isValidSignature(bytes,bytes)']("0xbaddad", "0x")).to.be.revertedWith("Hash not approved")
        })

        it('should revert if signature is not valid', async () => {
            const { validator } = await setupTests()
            await expect(validator.callStatic['isValidSignature(bytes,bytes)']("0xbaddad", "0xdeaddeaddeaddead")).to.be.reverted
        })

        it('should return magic value if message was signed', async () => {
            const { safe, validator, signLib } = await setupTests()
            await executeContractCallWithSigners(safe, signLib, "signMessage", ["0xbaddad"], [user1, user2], true)
            expect(await validator.callStatic['isValidSignature(bytes,bytes)']("0xbaddad", "0x")).to.be.eq("0x20c13b0b")
        })

        it('should return magic value if enough owners signed', async () => {
            const { validator } = await setupTests()
            const sig1 = {
                signer: user1.address,
                data: await user1._signTypedData({ verifyingContract: validator.address, chainId: await chainId() }, EIP712_SAFE_MESSAGE_TYPE, { message: "0xbaddad" })
            }
            const sig2 = await signHash(user2, calculateSafeMessageHash(validator, "0xbaddad", await chainId()))
            expect(await validator.callStatic['isValidSignature(bytes,bytes)']("0xbaddad", buildSignatureBytes([sig1, sig2]))).to.be.eq("0x20c13b0b")
        })
    })
    
    describe("isValidSignature(bytes32,bytes)", async () => {

        it('should revert if called directly', async () => {
            const { handler } = await setupTests()
            const dataHash = ethers.utils.keccak256("0xbaddad")
            await expect(handler.callStatic['isValidSignature(bytes32,bytes)'](dataHash, "0x")).to.be.revertedWith("function call to a non-contract account")
        })

        it('should revert if message was not signed', async () => {
            const { validator } = await setupTests()
            const dataHash = ethers.utils.keccak256("0xbaddad")
            await expect(validator.callStatic['isValidSignature(bytes32,bytes)'](dataHash, "0x")).to.be.revertedWith("Hash not approved")
        })

        it('should revert if signature is not valid', async () => {
            const { validator } = await setupTests()
            const dataHash = ethers.utils.keccak256("0xbaddad")
            await expect(validator.callStatic['isValidSignature(bytes32,bytes)'](dataHash, "0xdeaddeaddeaddead")).to.be.reverted
        })

        it('should return magic value if message was signed', async () => {
            const { safe, validator, signLib } = await setupTests()
            const dataHash = ethers.utils.keccak256("0xbaddad")
            await executeContractCallWithSigners(safe, signLib, "signMessage", [dataHash], [user1, user2], true)
            expect(await validator.callStatic['isValidSignature(bytes32,bytes)'](dataHash, "0x")).to.be.eq("0x1626ba7e")
        })

        it('should return magic value if enough owners signed', async () => {
            const { validator } = await setupTests()
            const dataHash = ethers.utils.keccak256("0xbaddad")
            const sig1 = {
                signer: user1.address,
                data: await user1._signTypedData({ verifyingContract: validator.address, chainId: await chainId() }, EIP712_SAFE_MESSAGE_TYPE, { message: dataHash })
            }
            const sig2 = await signHash(user2, calculateSafeMessageHash(validator,dataHash, await chainId()))
            expect(await validator.callStatic['isValidSignature(bytes32,bytes)'](dataHash, buildSignatureBytes([sig1, sig2]))).to.be.eq("0x1626ba7e")
        })
    })

    describe("getModules", async () => {
        it('returns enabled modules', async () => {
            const { safe, validator } = await setupTests()
            await expect(
                executeContractCallWithSigners(safe, safe, "enableModule", [user2.address], [user1, user2])
            ).to.emit(safe, "EnabledModule").withArgs(user2.address)

            await expect(
                await safe.isModuleEnabled(user2.address)
            ).to.be.true

            await expect(
                await validator.getModules()
            ).to.be.deep.equal([user2.address])
        })
    })

    describe("getMessageHash", async () => {
        it('should generate the correct hash', async () => {
            const { safe, validator } = await setupTests()
            expect(
                await validator.getMessageHash("0xdead")
            ).to.be.eq(calculateSafeMessageHash(safe, "0xdead", await chainId()))
        })
    })

    describe("getMessageHashForSafe", async () => {
        it('should revert if target does not return domain separator', async () => {
            const { handler } = await setupTests()
            await expect(
                handler.getMessageHashForSafe(handler.address, "0xdead")
            ).to.be.reverted
        })

        it('should generate the correct hash', async () => {
            const { handler, safe } = await setupTests()
            expect(
                await handler.getMessageHashForSafe(safe.address, "0xdead")
            ).to.be.eq(calculateSafeMessageHash(safe, "0xdead", await chainId()))
        })
    })

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

        it.skip('can be called for any Safe', async () => {
        })

        it('should revert changes', async () => {
            const { validator, killLib } = await setupTests()
            const code = await ethers.provider.getCode(validator.address)
            expect(
                await validator.callStatic.simulate(killLib.address, killLib.interface.encodeFunctionData("killme"))
            ).to.be.eq("0x")
            expect(
                await ethers.provider.getCode(validator.address)
            ).to.be.eq(code)
        })

        it('should return result', async () => {
            const { validator, killLib, handler } = await setupTests()
            expect(
                await validator.callStatic.simulate(killLib.address, killLib.interface.encodeFunctionData("expose"))
            ).to.be.eq("0x000000000000000000000000" + handler.address.slice(2).toLowerCase())
        })

        it('should propagate revert message', async () => {
            const { validator, killLib } = await setupTests()
            await expect(
                validator.callStatic.simulate(killLib.address, killLib.interface.encodeFunctionData("trever"))
            ).to.revertedWith("Why are you doing this?")
        })

        it('should simulate transaction', async () => {
            const { validator, killLib } = await setupTests()
            const estimate = await validator.callStatic.simulate(
                killLib.address,
                killLib.interface.encodeFunctionData("estimate", [validator.address, "0x"])
            )
            expect(BigNumber.from(estimate).toNumber()).to.be.lte(5000)
        })

        it('should return modified state', async () => {
            const { validator, killLib } = await setupTests()
            const value = await validator.callStatic.simulate(
                killLib.address,
                killLib.interface.encodeFunctionData("updateAndGet", [])
            )
            expect(BigNumber.from(value).toNumber()).to.be.eq(1)
            expect((await killLib.value()).toNumber()).to.be.eq(0)
        })
    })
})