import { expect } from './shared/setup'

/* 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 { Direction } from './shared/watcher-utils'

import { isMainnet, PROXY_SEQUENCER_ENTRYPOINT_ADDRESS } from './shared/utils'
import { OptimismEnv } from './shared/env'

const DEFAULT_TEST_GAS_L1 = 330_000
const DEFAULT_TEST_GAS_L2 = 1_300_000
// 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 sequencerBalance = await _env.ovmEth.balanceOf(
      PROXY_SEQUENCER_ENTRYPOINT_ADDRESS
    )
    const l1BridgeBalance = await _env.l1Wallet.provider.getBalance(
      _env.l1Bridge.address
    )

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

  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,
      }),
      Direction.L1ToL2
    )

    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,
      }),
      Direction.L1ToL2
    )

    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,
      }),
      Direction.L1ToL2
    )

    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,
      }),
      Direction.L1ToL2
    )

    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
  })

  it('withdraw', async function () {
    if (await isMainnet(env)) {
      console.log('Skipping withdrawals test on mainnet.')
      this.skip()
      return
    }

    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,
      Direction.L2ToL1
    )
    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 }
    )
  })

  it('withdrawTo', async function () {
    if (await isMainnet(env)) {
      console.log('Skipping withdrawals test on mainnet.')
      this.skip()
      return
    }

    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,
      Direction.L2ToL1
    )

    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'
    )
  })

  it('deposit, transfer, withdraw', async function () {
    if (await isMainnet(env)) {
      console.log('Skipping withdrawals test on mainnet.')
      this.skip()
      return
    }

    // 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,
      }),
      Direction.L1ToL2
    )

    // 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,
      Direction.L2ToL1
    )

    // 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))
  })
})
