pragma solidity 0.8.6;

import "./BaseTest.t.sol";
import {ChainSpecificUtil} from "../../ChainSpecificUtil_v0_8_6.sol";

import {ArbSys} from "../../vendor/@arbitrum/nitro-contracts/src/precompiles/ArbSys.sol";
import {ArbGasInfo} from "../../vendor/@arbitrum/nitro-contracts/src/precompiles/ArbGasInfo.sol";
import {OVM_GasPriceOracle} from "../../vendor/@eth-optimism/contracts/v0.8.6/contracts/L2/predeploys/OVM_GasPriceOracle.sol";

contract ChainSpecificUtilTest is BaseTest {
  // ------------ Start Arbitrum Constants ------------

  /// @dev ARBSYS_ADDR is the address of the ArbSys precompile on Arbitrum.
  /// @dev reference: https://github.com/OffchainLabs/nitro/blob/v2.0.14/contracts/src/precompiles/ArbSys.sol#L10
  address private constant ARBSYS_ADDR = address(0x0000000000000000000000000000000000000064);
  ArbSys private constant ARBSYS = ArbSys(ARBSYS_ADDR);

  /// @dev ARBGAS_ADDR is the address of the ArbGasInfo precompile on Arbitrum.
  /// @dev reference: https://github.com/OffchainLabs/nitro/blob/v2.0.14/contracts/src/precompiles/ArbGasInfo.sol#L10
  address private constant ARBGAS_ADDR = address(0x000000000000000000000000000000000000006C);
  ArbGasInfo private constant ARBGAS = ArbGasInfo(ARBGAS_ADDR);

  uint256 private constant ARB_MAINNET_CHAIN_ID = 42161;
  uint256 private constant ARB_GOERLI_TESTNET_CHAIN_ID = 421613;
  uint256 private constant ARB_SEPOLIA_TESTNET_CHAIN_ID = 421614;

  // ------------ End Arbitrum Constants ------------

  // ------------ Start Optimism Constants ------------
  /// @dev L1_FEE_DATA_PADDING includes 35 bytes for L1 data padding for Optimism
  bytes internal constant L1_FEE_DATA_PADDING =
    "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
  /// @dev OVM_GASPRICEORACLE_ADDR is the address of the OVM_GasPriceOracle precompile on Optimism.
  /// @dev reference: https://community.optimism.io/docs/developers/build/transaction-fees/#estimating-the-l1-data-fee
  address private constant OVM_GASPRICEORACLE_ADDR = address(0x420000000000000000000000000000000000000F);
  OVM_GasPriceOracle private constant OVM_GASPRICEORACLE = OVM_GasPriceOracle(OVM_GASPRICEORACLE_ADDR);

  uint256 private constant OP_MAINNET_CHAIN_ID = 10;
  uint256 private constant OP_GOERLI_CHAIN_ID = 420;
  uint256 private constant OP_SEPOLIA_CHAIN_ID = 11155420;

  /// @dev Base is a OP stack based rollup and follows the same L1 pricing logic as Optimism.
  uint256 private constant BASE_MAINNET_CHAIN_ID = 8453;
  uint256 private constant BASE_GOERLI_CHAIN_ID = 84531;

  // ------------ End Optimism Constants ------------

  function setUp() public override {
    BaseTest.setUp();
    vm.clearMockedCalls();
  }

  function testGetBlockhashArbitrum() public {
    uint256[3] memory chainIds = [ARB_MAINNET_CHAIN_ID, ARB_GOERLI_TESTNET_CHAIN_ID, ARB_SEPOLIA_TESTNET_CHAIN_ID];
    bytes32[3] memory expectedBlockHashes = [keccak256("mainnet"), keccak256("goerli"), keccak256("sepolia")];
    uint256[3] memory expectedBlockNumbers = [uint256(10), 11, 12];
    for (uint256 i = 0; i < chainIds.length; i++) {
      vm.chainId(chainIds[i]);
      bytes32 expectedBlockHash = expectedBlockHashes[i];
      uint256 expectedBlockNumber = expectedBlockNumbers[i];
      vm.mockCall(
        ARBSYS_ADDR,
        abi.encodeWithSelector(ArbSys.arbBlockNumber.selector),
        abi.encode(expectedBlockNumber + 1)
      );
      vm.mockCall(
        ARBSYS_ADDR,
        abi.encodeWithSelector(ArbSys.arbBlockHash.selector, expectedBlockNumber),
        abi.encodePacked(expectedBlockHash)
      );
      bytes32 actualBlockHash = ChainSpecificUtil._getBlockhash(uint64(expectedBlockNumber));
      assertEq(expectedBlockHash, actualBlockHash, "incorrect blockhash");
    }
  }

  function testGetBlockhashOptimism() public {
    // Optimism L2 block hash is simply blockhash()
    bytes32 actualBlockhash = ChainSpecificUtil._getBlockhash(uint64(block.number - 1));
    assertEq(blockhash(block.number - 1), actualBlockhash);
  }

  function testGetBlockNumberArbitrum() public {
    uint256[2] memory chainIds = [ARB_MAINNET_CHAIN_ID, ARB_GOERLI_TESTNET_CHAIN_ID];
    uint256[3] memory expectedBlockNumbers = [uint256(10), 11, 12];
    for (uint256 i = 0; i < chainIds.length; i++) {
      vm.chainId(chainIds[i]);
      uint256 expectedBlockNumber = expectedBlockNumbers[i];
      vm.mockCall(ARBSYS_ADDR, abi.encodeWithSelector(ArbSys.arbBlockNumber.selector), abi.encode(expectedBlockNumber));
      uint256 actualBlockNumber = ChainSpecificUtil._getBlockNumber();
      assertEq(expectedBlockNumber, actualBlockNumber, "incorrect block number");
    }
  }

  function testGetBlockNumberOptimism() public {
    // Optimism L2 block number is simply block.number
    uint256 actualBlockNumber = ChainSpecificUtil._getBlockNumber();
    assertEq(block.number, actualBlockNumber);
  }

  function testGetCurrentTxL1GasFeesArbitrum() public {
    uint256[3] memory chainIds = [ARB_MAINNET_CHAIN_ID, ARB_GOERLI_TESTNET_CHAIN_ID, ARB_SEPOLIA_TESTNET_CHAIN_ID];
    uint256[3] memory expectedGasFees = [uint256(10 gwei), 12 gwei, 14 gwei];
    for (uint256 i = 0; i < chainIds.length; i++) {
      vm.chainId(chainIds[i]);
      uint256 expectedGasFee = expectedGasFees[i];
      vm.mockCall(
        ARBGAS_ADDR,
        abi.encodeWithSelector(ArbGasInfo.getCurrentTxL1GasFees.selector),
        abi.encode(expectedGasFee)
      );
      uint256 actualGasFee = ChainSpecificUtil._getCurrentTxL1GasFees("");
      assertEq(expectedGasFee, actualGasFee, "incorrect gas fees");
    }
  }

  function testGetCurrentTxL1GasFeesOptimism() public {
    // set optimism chain id
    uint256[5] memory chainIds = [
      OP_MAINNET_CHAIN_ID,
      OP_GOERLI_CHAIN_ID,
      OP_SEPOLIA_CHAIN_ID,
      BASE_MAINNET_CHAIN_ID,
      BASE_GOERLI_CHAIN_ID
    ];
    uint256[5] memory expectedGasFees = [uint256(10 gwei), 12 gwei, 14 gwei, 16 gwei, 18 gwei];
    for (uint256 i = 0; i < chainIds.length; i++) {
      vm.chainId(chainIds[i]);
      uint256 expectedL1Fee = expectedGasFees[i];
      bytes memory someCalldata = abi.encode(address(0), "blah", uint256(1));
      vm.mockCall(
        OVM_GASPRICEORACLE_ADDR,
        abi.encodeWithSelector(OVM_GasPriceOracle.getL1Fee.selector, bytes.concat(someCalldata, L1_FEE_DATA_PADDING)),
        abi.encode(expectedL1Fee)
      );
      uint256 actualL1Fee = ChainSpecificUtil._getCurrentTxL1GasFees(someCalldata);
      assertEq(expectedL1Fee, actualL1Fee, "incorrect gas fees");
    }
  }

  function testGetL1CalldataGasCostArbitrum() public {
    uint256[3] memory chainIds = [ARB_MAINNET_CHAIN_ID, ARB_GOERLI_TESTNET_CHAIN_ID, ARB_SEPOLIA_TESTNET_CHAIN_ID];
    for (uint256 i = 0; i < chainIds.length; i++) {
      vm.chainId(chainIds[i]);
      vm.mockCall(
        ARBGAS_ADDR,
        abi.encodeWithSelector(ArbGasInfo.getPricesInWei.selector),
        abi.encode(0, 10, 0, 0, 0, 0)
      );

      // fee = l1PricePerByte * (calldataSizeBytes + 140)
      // fee = 10 * (10 + 140) = 1500
      uint256 dataFee = ChainSpecificUtil._getL1CalldataGasCost(10);
      assertEq(dataFee, 1500);
    }
  }

  function testGetL1CalldataGasCostOptimism() public {
    uint256[5] memory chainIds = [
      OP_MAINNET_CHAIN_ID,
      OP_GOERLI_CHAIN_ID,
      OP_SEPOLIA_CHAIN_ID,
      BASE_MAINNET_CHAIN_ID,
      BASE_GOERLI_CHAIN_ID
    ];
    for (uint256 i = 0; i < chainIds.length; i++) {
      vm.chainId(chainIds[i]);
      vm.mockCall(
        OVM_GASPRICEORACLE_ADDR,
        abi.encodeWithSelector(bytes4(hex"519b4bd3")), // l1BaseFee()
        abi.encode(10)
      );
      vm.mockCall(
        OVM_GASPRICEORACLE_ADDR,
        abi.encodeWithSelector(bytes4(hex"0c18c162")), // overhead()
        abi.encode(160)
      );
      vm.mockCall(
        OVM_GASPRICEORACLE_ADDR,
        abi.encodeWithSelector(bytes4(hex"f45e65d8")), // scalar()
        abi.encode(500_000)
      );
      vm.mockCall(
        OVM_GASPRICEORACLE_ADDR,
        abi.encodeWithSelector(bytes4(hex"313ce567")), // decimals()
        abi.encode(6)
      );

      // tx_data_gas = count_zero_bytes(tx_data) * 4 + count_non_zero_bytes(tx_data) * 16
      // tx_data_gas = 0 * 4 + 10 * 16 = 160
      // l1_data_fee = l1_gas_price * (tx_data_gas + fixed_overhead) * dynamic_overhead
      // l1_data_fee = 10 * (160 + 160) * 500_000 / 1_000_000 = 1600
      uint256 dataFee = ChainSpecificUtil._getL1CalldataGasCost(10);
      assertEq(dataFee, 1600);
    }
  }
}
