const { expect } = require("chai")
const { ethers } = require("hardhat")
const CONFIG = require("../constants/config.json")
const { getAddr, deployNew } = require("./util/helpers")
const { ZERO_ADDRESS } = require("./util/constants")

describe("Oft:", function () {
    this.timeout(300000)

    let owner, alice, bob, user3, user4, badUser1, fakeContract, router, mockToken, lzEndpoint, bridge
    let chainId, decimals, oft, lzEndpoints, users, mainEndpointId, numEndpoints, initSupplyMainEndpoint, name, symbol

    const getChainBalance = async (endpointId) => {
        return await oft[endpointId].balanceOf(oft[endpointId].address)
    }
    const getUserBalance = async (endpointId, userAddress) => {
        return await oft[endpointId].balanceOf(userAddress)
    }
    const auditGlobalTokenSupply = async function () {
        // make sure the total amount of tokens locked int the mainEndpoint token contract
        // is equal to the total tokens owned by all addresses on all non-mainEndpoint chains
        const mainLockedTokens = await getChainBalance(mainEndpointId)

        let childrenCumulativeTokens = ethers.BigNumber.from(0)
        for (let childEndpointId = 1; childEndpointId < numEndpoints; ++childEndpointId) {
            for (let userId = 0; userId < users.length; ++userId) {
                let childUserBalance = await getUserBalance(childEndpointId, users[userId].address)
                childrenCumulativeTokens = childrenCumulativeTokens.add(childUserBalance)
            }
        }

        expect(mainLockedTokens).to.equal(childrenCumulativeTokens)
    }
    const sendTokens = async (fromUserId, toUserId, qty, fromEndpointId, toEndpointId) => {
        const fromSigner = users[fromUserId]
        const toSigner = users[toUserId]

        const preFromBalance = await getUserBalance(fromEndpointId, fromSigner.address)
        const preToBalance = await getUserBalance(toEndpointId, toSigner.address)
        const localOft = oft[fromEndpointId]

        // send the tokens across chains
        await localOft.connect(fromSigner).sendTokens(toEndpointId, toSigner.address, qty, ZERO_ADDRESS, "0x", {
            value: ethers.utils.parseEther("0.1"),
        })

        const postFromBalance = await getUserBalance(fromEndpointId, fromSigner.address)
        const postToBalance = await getUserBalance(toEndpointId, toSigner.address)

        if (fromUserId === toUserId && fromEndpointId === toEndpointId) {
            // user sends to themselves on the same endpoint
            expect(postFromBalance).to.equal(preFromBalance)
            expect(postToBalance).to.equal(preToBalance)
        } else {
            // final balance check for a user send to a different user,
            // or a user sending to their address on another chain
            expect(postFromBalance).to.equal(preFromBalance.sub(qty))
            expect(postToBalance).to.equal(preToBalance.add(qty))
        }

        // audit the global token supply by comparing the locked tokens on mainendpoint to
        // the cumulative qty of tokens in user wallets on NON main endpoints
        await auditGlobalTokenSupply()
    }

    before(async function () {
        ;({ owner, alice, bob, user3, user4, badUser1, fakeContract } = await getAddr(ethers))
        users = [owner, alice, bob, user3, user4]
        chainId = 1
        decimals = 18
        initSupplyMainEndpoint = ethers.utils.parseEther("1000000000")
        name = CONFIG.stargateToken.name
        symbol = CONFIG.stargateToken.name

        mainEndpointId = 0 // endpoint id that controls the token
        numEndpoints = 3 // increase this number to iterate more endpoints
    })

    beforeEach(async function () {
        oft = {}
        lzEndpoints = {}

        for (let i = 0; i < numEndpoints; ++i) {
            lzEndpoints[i] = await deployNew("LZEndpointMock", [i])
            oft[i] = await deployNew("StargateToken", [name, symbol, lzEndpoints[i].address, mainEndpointId, initSupplyMainEndpoint])
        }

        // wire them together
        for (const i in oft) {
            for (const j in lzEndpoints) {
                // if (i === j) continue;
                const oftSrc = oft[i]
                const oftDst = oft[j]
                const lzSrc = lzEndpoints[i]
                const lzDst = lzEndpoints[j]

                // set
                await oftSrc.setDestination(j, oftDst.address)
                await oftDst.setDestination(i, oftSrc.address)
                await lzSrc.setDestLzEndpoint(oftDst.address, lzDst.address)
                await lzDst.setDestLzEndpoint(oftSrc.address, lzSrc.address)
            }
        }
    })

    it("send() - transfers to last chain", async function () {
        const fromUserId = 0
        const toUserId = 1
        const qty = ethers.utils.parseEther("1")
        const fromEndpointId = mainEndpointId
        const dstEndpointId = numEndpoints - 1
        await sendTokens(fromUserId, toUserId, qty, fromEndpointId, dstEndpointId)
    })

    it("send() - transfer to next chain", async function () {
        const fromUserId = 0
        const toUserId = 1
        const qty = ethers.utils.parseEther("1")
        const fromEndpointId = mainEndpointId
        const dstEndpointId = fromEndpointId + 1
        await sendTokens(fromUserId, toUserId, qty, fromEndpointId, dstEndpointId)
    })

    it("send() - send tokens to themselves", async function () {
        const user0 = 0
        await sendTokens(user0, user0, 100, mainEndpointId, mainEndpointId)
    })

    it("send() - everyone gets tokens, then everyone sends to everyone", async function () {
        // send everyone a balance on every endpoint from the source owner and source chain
        const originalOwnnerId = 0
        const originalMainEndpointId = mainEndpointId
        const largeQty = 1000000
        for (let toUserId = 0; toUserId < users.length; ++toUserId) {
            for (let endpointId = 0; endpointId < numEndpoints; ++endpointId) {
                await sendTokens(originalOwnnerId, toUserId, largeQty, originalMainEndpointId, endpointId)
            }
        }

        // have them all send to each other
        const qty = 100

        for (let fromUserId = 0; fromUserId < users.length; ++fromUserId) {
            for (let toUserId = 0; toUserId < users.length; ++toUserId) {
                for (let endpointId = 0; endpointId < numEndpoints; ++endpointId) {
                    for (let endpointId_B = 0; endpointId_B < numEndpoints; ++endpointId_B) {
                        await sendTokens(fromUserId, toUserId, qty, endpointId, endpointId_B)
                    }
                }
            }
        }
    })

    it("balanceOf() - initial balances of main and children (token contract)", async function () {
        // assert token balance. only chain 1 would mint
        for (let i = 0; i < numEndpoints; ++i) {
            expect(await getChainBalance(i)).to.equal("0")
        }
    })

    it("balanceOf() - initial balances of main and children (users)", async function () {
        //assert token balance. only chain 1 would mint
        for (let i = 0; i < numEndpoints; ++i) {
            for (let j = 0; j < users.length; ++j) {
                const balanceOft = await getUserBalance(i, users[j].address)

                if (i === mainEndpointId && j === 0) {
                    // the owner/deployer of the token on the main chain gets the initial supply
                    expect(balanceOft).to.equal("1000000000000000000000000000")
                } else {
                    expect(balanceOft).to.equal("0")
                }
            }
        }
    })

    it("sendTokens() - tokens get moved accross chains", async function () {
        // owner send some from oft_0 to alice oft_1
        // alice has no money to begin with
        expect(await oft[1].balanceOf(alice.address)).to.equal("0")
        const money1 = ethers.utils.parseEther("99")
        await oft[0].connect(owner).sendTokens(1, alice.address, money1, ZERO_ADDRESS, "0x", {
            value: ethers.utils.parseEther("0.1"),
        })
        // alice should have some money now
        expect(await oft[1].balanceOf(alice.address)).to.equal(money1)
        //assert token balance. only chain 1 would mint
        expect(await oft[0].totalSupply()).to.equal("1000000000000000000000000000")
        expect(await oft[1].totalSupply()).to.equal("99000000000000000000")

        // alice send some to bob from oft_1 to oft_2
        expect(await oft[2].balanceOf(bob.address)).to.equal("0")
        const money2 = ethers.utils.parseEther("15")
        await oft[1].connect(alice).sendTokens(2, bob.address, money2, ZERO_ADDRESS, "0x", {
            value: ethers.utils.parseEther("0.1"),
        })
        // alice should have some money now
        expect(await oft[2].balanceOf(bob.address)).to.equal(money2)

        // assert the total supply
        expect(await oft[0].totalSupply()).to.equal("1000000000000000000000000000")
        expect(await oft[1].totalSupply()).to.equal("84000000000000000000")
        expect(await oft[2].totalSupply()).to.equal("15000000000000000000")

        // bob send some to alice from oft_2 to oft_0
        const money3 = ethers.utils.parseEther("8")
        await oft[2].connect(bob).sendTokens(0, alice.address, money3, ZERO_ADDRESS, "0x", {
            value: ethers.utils.parseEther("0.1"),
        })
        // alice should have some money on oft[0]
        expect(await oft[0].balanceOf(alice.address)).to.equal(money3)

        // assert the total supply
        expect(await oft[0].totalSupply()).to.equal("1000000000000000000000000000")
        expect(await oft[1].totalSupply()).to.equal("84000000000000000000")
        // 15 - 8
        expect(await oft[2].totalSupply()).to.equal("7000000000000000000")
    })

    it("lzReceive() - reverts for non owner", async function () {
        // 42 is the arbitrary dummy return value in our mocks
        await expect(oft[0].lzReceive(chainId, "0x", 1, "0x")).to.revertedWith("")
    })

    it("estimateSendTokensFee()", async function () {
        // 42 is the arbitrary dummy return value in our mocks
        expect((await oft[0].estimateSendTokensFee(1, false, "0x"))[0]).to.equal(42)
    })

    it("setSendVersion()", async function () {
        const version = 22
        await oft[0].setSendVersion(version)
        expect(await lzEndpoints[0].mockSendVersion()).to.equal(version)
    })

    it("setReceiveVersion()", async function () {
        const version = 23
        await oft[0].setReceiveVersion(version)
        expect(await lzEndpoints[0].mockReceiveVersion()).to.equal(version)
    })

    it("setSendVersion() - reverts when non owner", async function () {
        const version = 22
        await expect(oft[0].connect(badUser1).setSendVersion(version)).to.revertedWith("Ownable: caller is not the owner")
    })

    it("setReceiveVersion() - reverts when non owner", async function () {
        const version = 23
        await expect(oft[0].connect(badUser1).setReceiveVersion(version)).to.revertedWith("Ownable: caller is not the owner")
    })

    it("setConfig()", async function () {
        const version = 22
        const configType = 0
        const config = "0x1234"
        await expect(oft[0].setConfig(version, chainId, configType, config))
            .to.emit(lzEndpoints[0], "SetConfig")
            .withArgs(version, chainId, configType, config)
    })

    it("setConfig() - reverts when non owner", async function () {
        const version = 22
        const configType = 0
        const config = "0x1234"
        await expect(oft[0].connect(badUser1).setConfig(version, chainId, configType, config)).to.revertedWith(
            "Ownable: caller is not the owner"
        )
    })

    it("forceResumeReceive()", async function () {
        const bytesAddr = "0x1234"
        await expect(oft[0].forceResumeReceive(chainId, bytesAddr)).to.emit(lzEndpoints[0], "ForceResumeReceive").withArgs(chainId, bytesAddr)
    })

    it("forceResumeReceive() - reverts when non owner", async function () {
        const bytesAddr = "0x1234"
        await expect(oft[0].connect(badUser1).forceResumeReceive(chainId, bytesAddr)).to.revertedWith("Ownable: caller is not the owner")
    })
})
