import { Contract, BigNumber } from 'ethers'
import { ethers } from 'hardhat'

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

export const traceToGasByOpcode = (structLogs, opcode) => {
  let gas = 0
  const opcodes = []
  for (const log of structLogs) {
    if (log.op === opcode) {
      opcodes.push(opcode)
      gas += log.gasCost
    }
  }
  return gas
}

describe('Hard forks', () => {
  let env: OptimismEnv
  let SimpleStorage: Contract
  let SelfDestruction: Contract
  let Precompiles: Contract

  before(async () => {
    env = await OptimismEnv.new()
    const Factory__SimpleStorage = await ethers.getContractFactory(
      'SimpleStorage',
      env.l2Wallet
    )
    SimpleStorage = await Factory__SimpleStorage.deploy()

    const Factory__SelfDestruction = await ethers.getContractFactory(
      'SelfDestruction',
      env.l2Wallet
    )
    SelfDestruction = await Factory__SelfDestruction.deploy()

    const Factory__Precompiles = await ethers.getContractFactory(
      'Precompiles',
      env.l2Wallet
    )
    Precompiles = await Factory__Precompiles.deploy()
  })

  describe('Berlin', () => {
    // https://eips.ethereum.org/EIPS/eip-2929
    describe('EIP-2929', () => {
      it('should update the gas schedule', async () => {
        // Get the tip height
        const tip = await env.l2Provider.getBlock('latest')

        // send a transaction to be able to trace
        const tx = await SimpleStorage.setValueNotXDomain(
          `0x${'77'.repeat(32)}`
        )
        await tx.wait()

        // Collect the traces
        const berlinTrace = await env.l2Provider.send(
          'debug_traceTransaction',
          [tx.hash]
        )
        const preBerlinTrace = await env.l2Provider.send(
          'debug_traceTransaction',
          [tx.hash, { overrides: { berlinBlock: tip.number * 2 } }]
        )
        expect(berlinTrace.gas).to.not.eq(preBerlinTrace.gas)

        const berlinSstoreCosts = traceToGasByOpcode(
          berlinTrace.structLogs,
          'SSTORE'
        )
        const preBerlinSstoreCosts = traceToGasByOpcode(
          preBerlinTrace.structLogs,
          'SSTORE'
        )
        expect(berlinSstoreCosts).to.not.eq(preBerlinSstoreCosts)
      })
    })

    // https://eips.ethereum.org/EIPS/eip-2565
    describe('EIP-2565', async () => {
      it('should become cheaper', async () => {
        const tip = await env.l2Provider.getBlock('latest')

        const tx = await Precompiles.expmod(64, 1, 64, { gasLimit: 5_000_000 })
        await tx.wait()

        const berlinTrace = await env.l2Provider.send(
          'debug_traceTransaction',
          [tx.hash]
        )
        const preBerlinTrace = await env.l2Provider.send(
          'debug_traceTransaction',
          [tx.hash, { overrides: { berlinBlock: tip.number * 2 } }]
        )
        expect(berlinTrace.gas).to.be.lt(preBerlinTrace.gas)
      })
    })
  })

  // Optimism includes EIP-3529 as part of its Berlin hardfork. It is part
  // of the London hardfork on L1. Since it is coupled to the Berlin
  // hardfork, some of its functionality cannot be directly tests via
  // integration tests since we can currently only turn on all of the Berlin
  // EIPs or none of the Berlin EIPs
  describe('Berlin Additional (L1 London)', () => {
    // https://eips.ethereum.org/EIPS/eip-3529
    describe('EIP-3529', async () => {
      const bytes32Zero = '0x' + '00'.repeat(32)
      const bytes32NonZero = '0x' + 'ff'.repeat(32)

      it('should lower the refund for storage clear', async () => {
        const tip = await env.l2Provider.getBlock('latest')

        const value = await SelfDestruction.callStatic.data()
        // It should be non zero
        expect(BigNumber.from(value).toNumber()).to.not.eq(0)

        {
          // Set the value to another non zero value
          // Going from non zero to non zero
          const tx = await SelfDestruction.setData(bytes32NonZero, {
            gasLimit: 5_000_000,
          })
          await tx.wait()

          const berlinTrace = await env.l2Provider.send(
            'debug_traceTransaction',
            [tx.hash]
          )
          const preBerlinTrace = await env.l2Provider.send(
            'debug_traceTransaction',
            [tx.hash, { overrides: { berlinBlock: tip.number * 2 } }]
          )
          // Updating a non zero value to another non zero value should not change
          expect(berlinTrace.gas).to.deep.eq(preBerlinTrace.gas)
        }

        {
          // Set the value to the zero value
          // Going from non zero to zero
          const tx = await SelfDestruction.setData(bytes32Zero, {
            gasLimit: 5_000_000,
          })
          await tx.wait()

          const berlinTrace = await env.l2Provider.send(
            'debug_traceTransaction',
            [tx.hash]
          )
          const preBerlinTrace = await env.l2Provider.send(
            'debug_traceTransaction',
            [tx.hash, { overrides: { berlinBlock: tip.number * 2 } }]
          )

          // Updating to a zero value from a non zero value should becomes
          // more expensive due to this change being coupled with EIP-2929
          expect(berlinTrace.gas).to.be.gt(preBerlinTrace.gas)
        }

        {
          // Set the value to a non zero value
          // Going from zero to non zero
          const tx = await SelfDestruction.setData(bytes32NonZero, {
            gasLimit: 5_000_000,
          })
          await tx.wait()

          const berlinTrace = await env.l2Provider.send(
            'debug_traceTransaction',
            [tx.hash]
          )
          const preBerlinTrace = await env.l2Provider.send(
            'debug_traceTransaction',
            [tx.hash, { overrides: { berlinBlock: tip.number * 2 } }]
          )

          // Updating to a zero value from a non zero value should becomes
          // more expensive due to this change being coupled with EIP-2929
          expect(berlinTrace.gas).to.be.gt(preBerlinTrace.gas)
        }
      })

      it('should remove the refund for selfdestruct', async () => {
        const tip = await env.l2Provider.getBlock('latest')

        // Send transaction with a large gas limit
        const tx = await SelfDestruction.destruct({ gasLimit: 5_000_000 })
        await tx.wait()

        const berlinTrace = await env.l2Provider.send(
          'debug_traceTransaction',
          [tx.hash]
        )
        const preBerlinTrace = await env.l2Provider.send(
          'debug_traceTransaction',
          [tx.hash, { overrides: { berlinBlock: tip.number * 2 } }]
        )

        // The berlin execution should use more gas than the pre Berlin
        // execution because there is no longer a selfdestruct gas
        // refund
        expect(berlinTrace.gas).to.be.gt(preBerlinTrace.gas)
      })
    })
  })
})
