/* Imports: External */
import { Contract, ContractFactory } from 'ethers'
import { ethers } from 'hardhat'
import { MessageDirection, MessageStatus } from '@eth-optimism/sdk'
import {
  applyL1ToL2Alias,
  awaitCondition,
  sleep,
} from '@eth-optimism/core-utils'

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

describe('Basic L1<>L2 Communication', async () => {
  let Factory__L1SimpleStorage: ContractFactory
  let Factory__L2SimpleStorage: ContractFactory
  let Factory__L2Reverter: ContractFactory
  let L1SimpleStorage: Contract
  let L2SimpleStorage: Contract
  let L2Reverter: Contract
  let env: OptimismEnv

  before(async () => {
    env = await OptimismEnv.new()
    Factory__L1SimpleStorage = await ethers.getContractFactory(
      'SimpleStorage',
      env.l1Wallet
    )
    Factory__L2SimpleStorage = await ethers.getContractFactory(
      'SimpleStorage',
      env.l2Wallet
    )
    Factory__L2Reverter = await ethers.getContractFactory(
      'Reverter',
      env.l2Wallet
    )
  })

  beforeEach(async () => {
    L1SimpleStorage = await Factory__L1SimpleStorage.deploy()
    await L1SimpleStorage.deployed()
    L2SimpleStorage = await Factory__L2SimpleStorage.deploy()
    await L2SimpleStorage.deployed()
    L2Reverter = await Factory__L2Reverter.deploy()
    await L2Reverter.deployed()
  })

  describe('L2 => L1', () => {
    withdrawalTest(
      'should be able to perform a withdrawal from L2 -> L1',
      async () => {
        const value = `0x${'77'.repeat(32)}`

        // Send L2 -> L1 message.
        const transaction = await env.messenger.sendMessage(
          {
            direction: MessageDirection.L2_TO_L1,
            target: L1SimpleStorage.address,
            message: L1SimpleStorage.interface.encodeFunctionData('setValue', [
              value,
            ]),
          },
          {
            overrides: {
              gasLimit: DEFAULT_TEST_GAS_L2,
            },
          }
        )

        let status: MessageStatus
        while (status !== MessageStatus.READY_FOR_RELAY) {
          status = await env.messenger.getMessageStatus(transaction)
          await sleep(1000)
        }

        await env.messenger.finalizeMessage(transaction)
        await env.messenger.waitForMessageReceipt(transaction)

        expect(await L1SimpleStorage.msgSender()).to.equal(
          env.messenger.contracts.l1.L1CrossDomainMessenger.address
        )
        expect(await L1SimpleStorage.xDomainSender()).to.equal(
          await env.messenger.l2Signer.getAddress()
        )
        expect(await L1SimpleStorage.value()).to.equal(value)
        expect((await L1SimpleStorage.totalCount()).toNumber()).to.equal(1)
      }
    )
  })

  describe('L1 => L2', () => {
    it('should deposit from L1 -> L2', async () => {
      const value = `0x${'42'.repeat(32)}`

      // Send L1 -> L2 message.
      const transaction = await env.messenger.sendMessage(
        {
          direction: MessageDirection.L1_TO_L2,
          target: L2SimpleStorage.address,
          message: L2SimpleStorage.interface.encodeFunctionData('setValue', [
            value,
          ]),
        },
        {
          l2GasLimit: 5000000,
          overrides: {
            gasLimit: DEFAULT_TEST_GAS_L1,
          },
        }
      )

      const receipt = await env.messenger.waitForMessageReceipt(transaction)

      expect(receipt.transactionReceipt.status).to.equal(1)
      expect(await L2SimpleStorage.msgSender()).to.equal(
        env.messenger.contracts.l2.L2CrossDomainMessenger.address
      )
      expect(await L2SimpleStorage.txOrigin()).to.equal(
        applyL1ToL2Alias(
          env.messenger.contracts.l1.L1CrossDomainMessenger.address
        )
      )
      expect(await L2SimpleStorage.xDomainSender()).to.equal(
        await env.messenger.l1Signer.getAddress()
      )
      expect(await L2SimpleStorage.value()).to.equal(value)
      expect((await L2SimpleStorage.totalCount()).toNumber()).to.equal(1)
    })

    it('should deposit from L1 -> L2 directly via enqueue', async function () {
      this.timeout(
        envConfig.MOCHA_TIMEOUT * 2 +
          envConfig.DTL_ENQUEUE_CONFIRMATIONS * 15000
      )
      const value = `0x${'42'.repeat(32)}`

      // Send L1 -> L2 message.
      const tx =
        await env.messenger.contracts.l1.CanonicalTransactionChain.connect(
          env.messenger.l1Signer
        ).enqueue(
          L2SimpleStorage.address,
          5000000,
          L2SimpleStorage.interface.encodeFunctionData('setValueNotXDomain', [
            value,
          ]),
          {
            gasLimit: DEFAULT_TEST_GAS_L1,
          }
        )

      const receipt = await tx.wait()

      const waitUntilBlock =
        receipt.blockNumber + envConfig.DTL_ENQUEUE_CONFIRMATIONS
      let currBlock = await env.messenger.l1Provider.getBlockNumber()
      while (currBlock <= waitUntilBlock) {
        const progress =
          envConfig.DTL_ENQUEUE_CONFIRMATIONS - (waitUntilBlock - currBlock)
        console.log(
          `Waiting for ${progress}/${envConfig.DTL_ENQUEUE_CONFIRMATIONS} confirmations.`
        )
        await sleep(5000)
        currBlock = await env.messenger.l1Provider.getBlockNumber()
      }
      console.log('Enqueue should be confirmed.')

      await awaitCondition(
        async () => {
          const sender = await L2SimpleStorage.msgSender()
          return sender === (await env.messenger.l1Signer.getAddress())
        },
        2000,
        60
      )

      // No aliasing when an EOA goes directly to L2.
      expect(await L2SimpleStorage.msgSender()).to.equal(
        await env.messenger.l1Signer.getAddress()
      )
      expect(await L2SimpleStorage.txOrigin()).to.equal(
        await env.messenger.l1Signer.getAddress()
      )
      expect(await L2SimpleStorage.value()).to.equal(value)
      expect((await L2SimpleStorage.totalCount()).toNumber()).to.equal(1)
    })
  })
})
