/* External Imports */
const { ethers } = require('hardhat')
const { expect } = require('chai')
const { Watcher } = require('@eth-optimism/watcher')
const { getContractFactory } = require('@eth-optimism/contracts')

/* Internal Imports */
const factory = (name, ovm = false) => {
  const artifact = require(`../artifacts${ovm ? '-ovm' : ''}/contracts/${name}.sol/${name}.json`)
  return new ethers.ContractFactory(artifact.abi, artifact.bytecode)
}
const factory__L1_ERC20 = factory('ERC20')
const factory__L2_ERC20 = factory('L2DepositedERC20', true)
const factory__L1StandardBridge = getContractFactory('OVM_L1StandardBridge')


describe(`L1 <> L2 Deposit and Withdrawal`, () => {
  // Set up our RPC provider connections.
  const l1RpcProvider = new ethers.providers.JsonRpcProvider('http://127.0.0.1:9545')
  const l2RpcProvider = new ethers.providers.JsonRpcProvider('http://127.0.0.1:8545')

  // Constructor arguments for `ERC20.sol`
  const INITIAL_SUPPLY = 1234
  const L1_ERC20_NAME = 'L1 ERC20'

  // L1 messenger address depends on the deployment, this is default for our local deployment.
  const l1MessengerAddress = '0x59b670e9fA9D0A427751Af201D676719a970857b'
  // L2 messenger address is always the same.
  const l2MessengerAddress = '0x4200000000000000000000000000000000000007'
  const L2_ERC20_NAME = 'L2 ERC20'

  // Set up our wallets (using a default private key with 10k ETH allocated to it).
  // Need two wallets objects, one for interacting with L1 and one for interacting with L2.
  // Both will use the same private key.
  const key = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
  const l1Wallet = new ethers.Wallet(key, l1RpcProvider)
  const l2Wallet = new ethers.Wallet(key, l2RpcProvider)

  // Tool that helps watches and waits for messages to be relayed between L1 and L2.
  const watcher = new Watcher({
    l1: {
      provider: l1RpcProvider,
      messengerAddress: l1MessengerAddress
    },
    l2: {
      provider: l2RpcProvider,
      messengerAddress: l2MessengerAddress
    }
  })

  let L1_ERC20,
    L2_ERC20,
    L1StandardBridge

  before(`deploy contracts`, async () => {
    // Deploy an ERC20 token on L1.
    L1_ERC20 = await factory__L1_ERC20.connect(l1Wallet).deploy(
      INITIAL_SUPPLY,
      L1_ERC20_NAME,
      {
        gasPrice: 0
      }
    )

    await L1_ERC20.deployTransaction.wait()

    // Deploy the paired ERC20 token to L2.
    L2_ERC20 = await factory__L2_ERC20.connect(l2Wallet).deploy(
      l2MessengerAddress,
      L2_ERC20_NAME,
      {
        gasPrice: 0
      }
    )

    await L2_ERC20.deployTransaction.wait()

    // Create a bridge that connects the two contracts.
    L1StandardBridge = await factory__L1StandardBridge.connect(l1Wallet).deploy(
      L1_ERC20.address,
      L2_ERC20.address,
      l1MessengerAddress,
      {
        gasPrice: 0
      }
    )

    await L1StandardBridge.deployTransaction.wait()
  })

  describe('Initialization and initial balances', async () => {
    it(`should initialize L2 ERC20`, async () => {
      const tx = await L2_ERC20.init(L1StandardBridge.address, { gasPrice: 0 })
      await tx.wait()
      const txHashPrefix = tx.hash.slice(0, 2)
      expect(txHashPrefix).to.eq('0x')
    })

    it(`should have initial L1 balance of 1234 and initial L2 balance of 0`, async () => {
      const l1Balance = await L1_ERC20.balanceOf(l1Wallet.address)
      const l2Balance = await L2_ERC20.balanceOf(l1Wallet.address)
      expect(l1Balance).to.eq(1234)
      expect(l2Balance).to.eq(0)
    })
  })

  describe('L1 to L2 deposit', async () => {
    let l1Tx1

    it(`should approve 1234 tokens for ERC20 bridge`, async () => {
      const tx = await L1_ERC20.approve(L1StandardBridge.address, 1234)
      await tx.wait()
      const txHashPrefix = tx.hash.slice(0, 2)
      expect(txHashPrefix).to.eq('0x')
    })

    it(`should deposit 1234 tokens into L2 ERC20`, async () => {
      l1Tx1 = await L1StandardBridge.deposit(1234, { gasPrice: 0 })
      await l1Tx1.wait()
      const txHashPrefix = l1Tx1.hash.slice(0, 2)
      expect(txHashPrefix).to.eq('0x')
    })

    it(`should relay deposit message to L2`, async () => {
      // Wait for the message to be relayed to L2.
      const [ msgHash1 ] = await watcher.getMessageHashesFromL1Tx(l1Tx1.hash)
      const l2TxReceipt = await watcher.getL2TransactionReceipt(msgHash1)
      expect(l2TxReceipt.to).to.eq(l2MessengerAddress)
    })

    it(`should have changed L1 balance to 0 and L2 balance to 1234`, async () => {
      const l1Balance = await L1_ERC20.balanceOf(l1Wallet.address)
      const l2Balance = await L2_ERC20.balanceOf(l1Wallet.address)
      expect(l1Balance).to.eq(0)
      expect(l2Balance).to.eq(1234)
    })
  })

  describe('L2 to L1 withdrawal', async () => {
    let l2Tx1

    it(`should withdraw tokens back to L1 ERC20 and relay the message`, async () => {
      // Burn the tokens on L2 and ask the L1 contract to unlock on our behalf.
      l2Tx1 = await L2_ERC20.withdraw(1234, { gasPrice: 0 })
      await l2Tx1.wait()
      const txHashPrefix = l2Tx1.hash.slice(0, 2)
      expect(txHashPrefix).to.eq('0x')
    })

    it(`should relay withdrawal message to L1`, async () => {
      const [ msgHash2 ] = await watcher.getMessageHashesFromL2Tx(l2Tx1.hash)
      const l1TxReceipt = await watcher.getL1TransactionReceipt(msgHash2)
      expect(l1TxReceipt.to).to.eq(l1MessengerAddress)
    })

    it(`should have changed L1 balance back to 1234 and L2 balance back to 0`, async () => {
      const l1Balance = await L1_ERC20.balanceOf(l1Wallet.address)
      const l2Balance = await L2_ERC20.balanceOf(l1Wallet.address)
      expect(l1Balance).to.eq(1234)
      expect(l2Balance).to.eq(0)
    })
  })
})
