/* Imports: External */
import { Wallet, utils, BigNumber } from 'ethers'
import { serialize } from '@ethersproject/transactions'
import { predeploys } from '@eth-optimism/contracts'
import { expectApprox } from '@eth-optimism/core-utils'

/* Imports: Internal */
import { expect } from './shared/setup'
import {
  DEFAULT_TEST_GAS_L1,
  DEFAULT_TEST_GAS_L2,
  envConfig,
  withdrawalTest,
} from './shared/utils'
import { OptimismEnv } from './shared/env'

// TX size enforced by CTC:
const MAX_ROLLUP_TX_SIZE = 50_000

describe('Native ETH Integration Tests', async () => {
  let env: OptimismEnv
  let l1Bob: Wallet
  let l2Bob: Wallet

  const getBalances = async (_env: OptimismEnv) => {
    const l1UserBalance = await _env.l1Wallet.getBalance()
    const l2UserBalance = await _env.l2Wallet.getBalance()

    const l1BobBalance = await l1Bob.getBalance()
    const l2BobBalance = await l2Bob.getBalance()

    const l1BridgeBalance = await _env.l1Wallet.provider.getBalance(
      _env.l1Bridge.address
    )

    return {
      l1UserBalance,
      l2UserBalance,
      l1BobBalance,
      l2BobBalance,
      l1BridgeBalance,
    }
  }

  before(async () => {
    env = await OptimismEnv.new()
    l1Bob = Wallet.createRandom().connect(env.l1Wallet.provider)
    l2Bob = l1Bob.connect(env.l2Wallet.provider)
  })

  describe('estimateGas', () => {
    it('Should estimate gas for ETH withdraw', async () => {
      const amount = utils.parseEther('0.0000001')
      const gas = await env.l2Bridge.estimateGas.withdraw(
        predeploys.OVM_ETH,
        amount,
        0,
        '0xFFFF'
      )
      // Expect gas to be less than or equal to the target plus 1%
      expectApprox(gas, 6700060, { absoluteUpperDeviation: 1000 })
    })
  })

  it('receive', async () => {
    const depositAmount = 10
    const preBalances = await getBalances(env)
    const { tx, receipt } = await env.waitForXDomainTransaction(
      env.l1Wallet.sendTransaction({
        to: env.l1Bridge.address,
        value: depositAmount,
        gasLimit: DEFAULT_TEST_GAS_L1,
      })
    )

    const l1FeePaid = receipt.gasUsed.mul(tx.gasPrice)
    const postBalances = await getBalances(env)

    expect(postBalances.l1BridgeBalance).to.deep.eq(
      preBalances.l1BridgeBalance.add(depositAmount)
    )
    expect(postBalances.l2UserBalance).to.deep.eq(
      preBalances.l2UserBalance.add(depositAmount)
    )
    expect(postBalances.l1UserBalance).to.deep.eq(
      preBalances.l1UserBalance.sub(l1FeePaid.add(depositAmount))
    )
  })

  it('depositETH', async () => {
    const depositAmount = 10
    const preBalances = await getBalances(env)
    const { tx, receipt } = await env.waitForXDomainTransaction(
      env.l1Bridge.depositETH(DEFAULT_TEST_GAS_L2, '0xFFFF', {
        value: depositAmount,
        gasLimit: DEFAULT_TEST_GAS_L1,
      })
    )

    const l1FeePaid = receipt.gasUsed.mul(tx.gasPrice)
    const postBalances = await getBalances(env)

    expect(postBalances.l1BridgeBalance).to.deep.eq(
      preBalances.l1BridgeBalance.add(depositAmount)
    )
    expect(postBalances.l2UserBalance).to.deep.eq(
      preBalances.l2UserBalance.add(depositAmount)
    )
    expect(postBalances.l1UserBalance).to.deep.eq(
      preBalances.l1UserBalance.sub(l1FeePaid.add(depositAmount))
    )
  })

  it('depositETHTo', async () => {
    const depositAmount = 10
    const preBalances = await getBalances(env)
    const depositReceipts = await env.waitForXDomainTransaction(
      env.l1Bridge.depositETHTo(l2Bob.address, DEFAULT_TEST_GAS_L2, '0xFFFF', {
        value: depositAmount,
        gasLimit: DEFAULT_TEST_GAS_L1,
      })
    )

    const l1FeePaid = depositReceipts.receipt.gasUsed.mul(
      depositReceipts.tx.gasPrice
    )
    const postBalances = await getBalances(env)
    expect(postBalances.l1BridgeBalance).to.deep.eq(
      preBalances.l1BridgeBalance.add(depositAmount)
    )
    expect(postBalances.l2BobBalance).to.deep.eq(
      preBalances.l2BobBalance.add(depositAmount)
    )
    expect(postBalances.l1UserBalance).to.deep.eq(
      preBalances.l1UserBalance.sub(l1FeePaid.add(depositAmount))
    )
  })

  it('deposit passes with a large data argument', async () => {
    const ASSUMED_L2_GAS_LIMIT = 8_000_000
    const depositAmount = 10
    const preBalances = await getBalances(env)

    // Set data length slightly less than MAX_ROLLUP_TX_SIZE
    // to allow for encoding and other arguments
    const data = `0x` + 'ab'.repeat(MAX_ROLLUP_TX_SIZE - 500)
    const { tx, receipt } = await env.waitForXDomainTransaction(
      env.l1Bridge.depositETH(ASSUMED_L2_GAS_LIMIT, data, {
        value: depositAmount,
        gasLimit: 4_000_000,
      })
    )

    const l1FeePaid = receipt.gasUsed.mul(tx.gasPrice)
    const postBalances = await getBalances(env)
    expect(postBalances.l1BridgeBalance).to.deep.eq(
      preBalances.l1BridgeBalance.add(depositAmount)
    )
    expect(postBalances.l2UserBalance).to.deep.eq(
      preBalances.l2UserBalance.add(depositAmount)
    )
    expect(postBalances.l1UserBalance).to.deep.eq(
      preBalances.l1UserBalance.sub(l1FeePaid.add(depositAmount))
    )
  })

  it('depositETH fails with a TOO large data argument', async () => {
    const depositAmount = 10

    const data = `0x` + 'ab'.repeat(MAX_ROLLUP_TX_SIZE + 1)
    await expect(
      env.l1Bridge.depositETH(DEFAULT_TEST_GAS_L2, data, {
        value: depositAmount,
      })
    ).to.be.reverted
  })

  withdrawalTest('withdraw', async () => {
    const withdrawAmount = BigNumber.from(3)
    const preBalances = await getBalances(env)
    expect(
      preBalances.l2UserBalance.gt(0),
      'Cannot run withdrawal test before any deposits...'
    )

    const transaction = await env.l2Bridge.withdraw(
      predeploys.OVM_ETH,
      withdrawAmount,
      DEFAULT_TEST_GAS_L2,
      '0xFFFF'
    )
    await transaction.wait()
    await env.relayXDomainMessages(transaction)
    const receipts = await env.waitForXDomainTransaction(transaction)
    const fee = receipts.tx.gasLimit.mul(receipts.tx.gasPrice)

    const postBalances = await getBalances(env)

    // Approximate because there's a fee related to relaying the L2 => L1 message and it throws off the math.
    expectApprox(
      postBalances.l1BridgeBalance,
      preBalances.l1BridgeBalance.sub(withdrawAmount),
      { percentUpperDeviation: 1 }
    )
    expectApprox(
      postBalances.l2UserBalance,
      preBalances.l2UserBalance.sub(withdrawAmount.add(fee)),
      { percentUpperDeviation: 1 }
    )
    expectApprox(
      postBalances.l1UserBalance,
      preBalances.l1UserBalance.add(withdrawAmount),
      { percentUpperDeviation: 1 }
    )
  })

  withdrawalTest('withdrawTo', async () => {
    const withdrawAmount = BigNumber.from(3)

    const preBalances = await getBalances(env)

    expect(
      preBalances.l2UserBalance.gt(0),
      'Cannot run withdrawal test before any deposits...'
    )

    const transaction = await env.l2Bridge.withdrawTo(
      predeploys.OVM_ETH,
      l1Bob.address,
      withdrawAmount,
      DEFAULT_TEST_GAS_L2,
      '0xFFFF'
    )

    await transaction.wait()
    await env.relayXDomainMessages(transaction)
    const receipts = await env.waitForXDomainTransaction(transaction)

    const l2Fee = receipts.tx.gasPrice.mul(receipts.receipt.gasUsed)

    // Calculate the L1 portion of the fee
    const raw = serialize({
      nonce: transaction.nonce,
      value: transaction.value,
      gasPrice: transaction.gasPrice,
      gasLimit: transaction.gasLimit,
      to: transaction.to,
      data: transaction.data,
    })

    const l1Fee = await env.gasPriceOracle.getL1Fee(raw)
    const fee = l2Fee.add(l1Fee)

    const postBalances = await getBalances(env)

    expect(postBalances.l1BridgeBalance).to.deep.eq(
      preBalances.l1BridgeBalance.sub(withdrawAmount),
      'L1 Bridge Balance Mismatch'
    )

    expect(postBalances.l2UserBalance).to.deep.eq(
      preBalances.l2UserBalance.sub(withdrawAmount.add(fee)),
      'L2 User Balance Mismatch'
    )

    expect(postBalances.l1BobBalance).to.deep.eq(
      preBalances.l1BobBalance.add(withdrawAmount),
      'L1 User Balance Mismatch'
    )
  })

  withdrawalTest(
    'deposit, transfer, withdraw',
    async () => {
      // 1. deposit
      const amount = utils.parseEther('1')
      await env.waitForXDomainTransaction(
        env.l1Bridge.depositETH(DEFAULT_TEST_GAS_L2, '0xFFFF', {
          value: amount,
          gasLimit: DEFAULT_TEST_GAS_L1,
        })
      )

      // 2. transfer to another address
      const other = Wallet.createRandom().connect(env.l2Wallet.provider)
      const tx = await env.l2Wallet.sendTransaction({
        to: other.address,
        value: amount,
      })
      await tx.wait()

      const l1BalanceBefore = await other
        .connect(env.l1Wallet.provider)
        .getBalance()

      // 3. do withdrawal
      const withdrawnAmount = utils.parseEther('0.95')
      const transaction = await env.l2Bridge
        .connect(other)
        .withdraw(
          predeploys.OVM_ETH,
          withdrawnAmount,
          DEFAULT_TEST_GAS_L1,
          '0xFFFF'
        )
      await transaction.wait()
      await env.relayXDomainMessages(transaction)
      const receipts = await env.waitForXDomainTransaction(transaction)

      // Compute the L1 portion of the fee
      const l1Fee = await env.gasPriceOracle.getL1Fee(
        serialize({
          nonce: transaction.nonce,
          value: transaction.value,
          gasPrice: transaction.gasPrice,
          gasLimit: transaction.gasLimit,
          to: transaction.to,
          data: transaction.data,
        })
      )

      // check that correct amount was withdrawn and that fee was charged
      const l2Fee = receipts.tx.gasPrice.mul(receipts.receipt.gasUsed)

      const fee = l1Fee.add(l2Fee)
      const l1BalanceAfter = await other
        .connect(env.l1Wallet.provider)
        .getBalance()
      const l2BalanceAfter = await other.getBalance()
      expect(l1BalanceAfter).to.deep.eq(l1BalanceBefore.add(withdrawnAmount))
      expect(l2BalanceAfter).to.deep.eq(amount.sub(withdrawnAmount).sub(fee))
    },
    envConfig.MOCHA_TIMEOUT * 3
  )
})
