/*
 * Copyright ConsenSys AG.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 *
 * SPDX-License-Identifier: Apache-2.0
 */
package org.hyperledger.besu.ethereum.chain;

import static org.assertj.core.api.Assertions.assertThat;

import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.ethereum.core.BlockHeader;
import org.hyperledger.besu.ethereum.core.InMemoryKeyValueStorageProvider;
import org.hyperledger.besu.ethereum.core.MutableWorldState;
import org.hyperledger.besu.ethereum.core.ProtocolScheduleFixture;
import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput;
import org.hyperledger.besu.ethereum.worldstate.DataStorageConfiguration;
import org.hyperledger.besu.evm.account.Account;

import java.util.stream.Stream;

import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.units.bigints.UInt256;
import org.bouncycastle.util.encoders.Hex;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.ArgumentsProvider;
import org.junit.jupiter.params.provider.ArgumentsSource;

final class GenesisStateTest {

  /** Known RLP encoded bytes of the Olympic Genesis Block. */
  private static final String OLYMPIC_RLP =
      "f901f8f901f3a00000000000000000000000000000000000000000000000000000000000000000a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347940000000000000000000000000000000000000000a09178d0f23c965d81f0834a4c72c6253ce6830f4022b1359aaebfc1ecba442d4ea056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008302000080832fefd8808080a0000000000000000000000000000000000000000000000000000000000000000088000000000000002ac0c0";

  /** Known Hash of the Olympic Genesis Block. */
  private static final String OLYMPIC_HASH =
      "fd4af92a79c7fc2fd8bf0d342f2e832e1d4f485c85b9152d2039e03bc604fdca";

  private static final String EXPECTED_CODE =
      " ";

  static class GenesisStateTestArguments implements ArgumentsProvider {
    @Override
    public Stream<? extends Arguments> provideArguments(final ExtensionContext context) {
      return Stream.of(
          Arguments.of(DataStorageConfiguration.DEFAULT_FOREST_CONFIG),
          Arguments.of(DataStorageConfiguration.DEFAULT_BONSAI_CONFIG));
    }
  }

  @ParameterizedTest
  @ArgumentsSource(GenesisStateTestArguments.class)
  public void createFromJsonWithAllocs(final DataStorageConfiguration dataStorageConfiguration) {
    final GenesisState genesisState =
        GenesisState.fromJsonSource(
            dataStorageConfiguration,
            GenesisStateTest.class.getResource("genesis1.json"),
            ProtocolScheduleFixture.MAINNET);
    final BlockHeader header = genesisState.getBlock().getHeader();
    assertThat(header.getStateRoot())
        .isEqualTo(
            Hash.fromHexString(
                "0x92683e6af0f8a932e5fe08c870f2ae9d287e39d4518ec544b0be451f1035fd39"));
    assertThat(header.getTransactionsRoot()).isEqualTo(Hash.EMPTY_TRIE_HASH);
    assertThat(header.getReceiptsRoot()).isEqualTo(Hash.EMPTY_TRIE_HASH);
    assertThat(header.getOmmersHash()).isEqualTo(Hash.EMPTY_LIST_HASH);
    assertThat(header.getExtraData()).isEqualTo(Bytes.EMPTY);
    assertThat(header.getParentHash()).isEqualTo(Hash.ZERO);
    final MutableWorldState worldState = InMemoryKeyValueStorageProvider.createInMemoryWorldState();
    genesisState.writeStateTo(worldState);
    final Account first =
        worldState.get(Address.fromHexString("0x0000000000000000000000000000000000000001"));
    final Account second =
        worldState.get(Address.fromHexString("0x0000000000000000000000000000000000000002"));
    assertThat(first).isNotNull();
    assertThat(first.getBalance().toLong()).isEqualTo(111111111);
    assertThat(second).isNotNull();
    assertThat(second.getBalance().toLong()).isEqualTo(222222222);
  }

  @ParameterizedTest
  @ArgumentsSource(GenesisStateTestArguments.class)
  void createFromJsonNoAllocs(final DataStorageConfiguration dataStorageConfiguration) {
    final GenesisState genesisState =
        GenesisState.fromJsonSource(
            dataStorageConfiguration,
            GenesisStateTest.class.getResource("genesis2.json"),
            ProtocolScheduleFixture.MAINNET);
    final BlockHeader header = genesisState.getBlock().getHeader();
    assertThat(header.getStateRoot()).isEqualTo(Hash.EMPTY_TRIE_HASH);
    assertThat(header.getTransactionsRoot()).isEqualTo(Hash.EMPTY_TRIE_HASH);
    assertThat(header.getReceiptsRoot()).isEqualTo(Hash.EMPTY_TRIE_HASH);
    assertThat(header.getOmmersHash()).isEqualTo(Hash.EMPTY_LIST_HASH);
    assertThat(header.getExtraData()).isEqualTo(Bytes.EMPTY);
    assertThat(header.getParentHash()).isEqualTo(Hash.ZERO);
  }

  private void assertContractInvariants(
      final DataStorageConfiguration dataStorageConfiguration,
      final String sourceFile,
      final String blockHash) {
    final GenesisState genesisState =
        GenesisState.fromJsonSource(
            dataStorageConfiguration,
            GenesisStateTest.class.getResource(sourceFile),
            ProtocolScheduleFixture.MAINNET);
    final BlockHeader header = genesisState.getBlock().getHeader();
    assertThat(header.getHash()).isEqualTo(Hash.fromHexString(blockHash));

    final MutableWorldState worldState = InMemoryKeyValueStorageProvider.createInMemoryWorldState();
    genesisState.writeStateTo(worldState);
    final Account contract =
        worldState.get(Address.fromHexString("0x3850000000000000000000000000000000000000"));
    assertThat(contract.getCode()).isEqualTo(Bytes.fromHexString(EXPECTED_CODE));
    assertStorageValue(
        contract,
        "c2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85d",
        "00000000000000000000000038500f1002084341bf47ee913c4dc2cd92ede0ea");
    assertStorageValue(
        contract,
        "c2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f860",
        "000000000000000000000000385ef55e292fa39cf5ffbad99f534294565519ba");
  }

  @ParameterizedTest
  @ArgumentsSource(GenesisStateTestArguments.class)
  void createFromJsonWithContract(final DataStorageConfiguration dataStorageConfiguration) {
    assertContractInvariants(
        dataStorageConfiguration,
        "genesis3.json",
        "0xe7fd8db206dcaf066b7c97b8a42a0abc18653613560748557ab44868652a78b6");
  }

  @ParameterizedTest
  @ArgumentsSource(GenesisStateTestArguments.class)
  void createFromJsonWithNonce(final DataStorageConfiguration dataStorageConfiguration) {
    final GenesisState genesisState =
        GenesisState.fromJsonSource(
            dataStorageConfiguration,
            GenesisStateTest.class.getResource("genesisNonce.json"),
            ProtocolScheduleFixture.MAINNET);
    final BlockHeader header = genesisState.getBlock().getHeader();
    assertThat(header.getHash())
        .isEqualTo(
            Hash.fromHexString(
                "0x36750291f1a8429aeb553a790dc2d149d04dbba0ca4cfc7fd5eb12d478117c9f"));
  }

  @ParameterizedTest
  @ArgumentsSource(GenesisStateTestArguments.class)
  void encodeOlympicBlock(final DataStorageConfiguration dataStorageConfiguration) {
    final GenesisState genesisState =
        GenesisState.fromJsonSource(
            dataStorageConfiguration,
            GenesisStateTest.class.getResource("genesis-olympic.json"),
            ProtocolScheduleFixture.MAINNET);
    final BytesValueRLPOutput tmp = new BytesValueRLPOutput();
    genesisState.getBlock().writeTo(tmp);
    assertThat(Hex.toHexString(genesisState.getBlock().getHeader().getHash().toArray()))
        .isEqualTo(OLYMPIC_HASH);
    assertThat(Hex.toHexString(tmp.encoded().toArray())).isEqualTo(OLYMPIC_RLP);
  }

  private void assertStorageValue(final Account contract, final String key, final String value) {
    assertThat(contract.getStorageValue(UInt256.fromHexString(key)))
        .isEqualTo(UInt256.fromHexString(value));
  }

  @ParameterizedTest
  @ArgumentsSource(GenesisStateTestArguments.class)
  void genesisFromShanghai(final DataStorageConfiguration dataStorageConfiguration) {
    final GenesisState genesisState =
        GenesisState.fromJsonSource(
            dataStorageConfiguration,
            GenesisStateTest.class.getResource("genesis_shanghai.json"),
            ProtocolScheduleFixture.MAINNET);
    final BlockHeader header = genesisState.getBlock().getHeader();
    assertThat(header.getHash())
        .isEqualTo(
            Hash.fromHexString(
                "0xfdc41f92053811b877be43e61cab6b0d9ee55501ae2443df0970c753747f12d8"));
    assertThat(header.getGasLimit()).isEqualTo(0x2fefd8);
    assertThat(header.getGasUsed()).isZero();
    assertThat(header.getNumber()).isZero();
    assertThat(header.getReceiptsRoot())
        .isEqualTo(
            Hash.fromHexString(
                "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"));
    assertThat(header.getTransactionsRoot()).isEqualTo(Hash.EMPTY_TRIE_HASH);
    assertThat(header.getOmmersHash())
        .isEqualTo(
            Hash.fromHexString(
                "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347"));
    assertThat(header.getExtraData()).isEqualTo(Bytes.EMPTY);
    assertThat(header.getParentHash()).isEqualTo(Hash.ZERO);

    final MutableWorldState worldState = InMemoryKeyValueStorageProvider.createInMemoryWorldState();
    genesisState.writeStateTo(worldState);
    Hash computedStateRoot = worldState.rootHash();
    assertThat(computedStateRoot).isEqualTo(header.getStateRoot());
    assertThat(header.getStateRoot())
        .isEqualTo(
            Hash.fromHexString(
                "0x7f5cfe1375a61009a22d24512d18035bc8f855129452fa9c6a6be2ef4e9da7db"));
    final Account first =
        worldState.get(Address.fromHexString("0000000000000000000000000000000000000100"));
    final Account last =
        worldState.get(Address.fromHexString("fb289e2b2b65fb63299a682d000744671c50417b"));
    assertThat(first).isNotNull();
    assertThat(first.getBalance().toLong()).isZero();
    assertThat(first.getCode())
        .isEqualTo(Bytes.fromHexString("0x5f804955600180495560028049556003804955"));
    assertThat(last).isNotNull();
    Wei lastBalance = last.getBalance();
    assertThat(lastBalance).isEqualTo(Wei.fromHexString("0x123450000000000000000"));
  }

  @ParameterizedTest
  @ArgumentsSource(GenesisStateTestArguments.class)
  void genesisFromCancun(final DataStorageConfiguration dataStorageConfiguration) {
    final GenesisState genesisState =
        GenesisState.fromJsonSource(
            dataStorageConfiguration,
            GenesisStateTest.class.getResource("genesis_cancun.json"),
            ProtocolScheduleFixture.MAINNET);
    final BlockHeader header = genesisState.getBlock().getHeader();
    assertThat(header.getHash())
        .isEqualTo(
            Hash.fromHexString(
                "0x87846b86c1026fa7d7be2da045716274231de1871065a320659c9b111287c688"));
    assertThat(header.getGasLimit()).isEqualTo(0x2fefd8);
    assertThat(header.getGasUsed()).isZero();
    assertThat(header.getNumber()).isZero();
    assertThat(header.getReceiptsRoot())
        .isEqualTo(
            Hash.fromHexString(
                "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"));
    assertThat(header.getTransactionsRoot()).isEqualTo(Hash.EMPTY_TRIE_HASH);
    assertThat(header.getOmmersHash())
        .isEqualTo(
            Hash.fromHexString(
                "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347"));
    assertThat(header.getExtraData()).isEqualTo(Bytes.EMPTY);
    assertThat(header.getParentHash()).isEqualTo(Hash.ZERO);

    final MutableWorldState worldState = InMemoryKeyValueStorageProvider.createInMemoryWorldState();
    genesisState.writeStateTo(worldState);
    Hash computedStateRoot = worldState.rootHash();
    assertThat(computedStateRoot).isEqualTo(header.getStateRoot());
    assertThat(header.getStateRoot())
        .isEqualTo(
            Hash.fromHexString(
                "0x7f5cfe1375a61009a22d24512d18035bc8f855129452fa9c6a6be2ef4e9da7db"));
    final Account first =
        worldState.get(Address.fromHexString("0000000000000000000000000000000000000100"));
    final Account last =
        worldState.get(Address.fromHexString("fb289e2b2b65fb63299a682d000744671c50417b"));
    assertThat(first).isNotNull();
    assertThat(first.getBalance().toLong()).isZero();
    assertThat(first.getCode())
        .isEqualTo(Bytes.fromHexString("0x5f804955600180495560028049556003804955"));
    assertThat(last).isNotNull();
    Wei lastBalance = last.getBalance();
    assertThat(lastBalance).isEqualTo(Wei.fromHexString("0x123450000000000000000"));
    assertThat(header.getRequestsHash().isPresent()).isFalse();
  }

  @ParameterizedTest
  @ArgumentsSource(GenesisStateTestArguments.class)
  void genesisFromPrague(final DataStorageConfiguration dataStorageConfiguration) {
    final GenesisState genesisState =
        GenesisState.fromJsonSource(
            dataStorageConfiguration,
            GenesisStateTest.class.getResource("genesis_prague.json"),
            ProtocolScheduleFixture.MAINNET);
    final BlockHeader header = genesisState.getBlock().getHeader();
    assertThat(header.getHash())
        .isEqualTo(
            Hash.fromHexString(
                "0x5d2d02fce02d1b7ca635ec91a4fe6f7aa36f9b3997ec4304e8c68d8f6f15d266"));
    assertThat(header.getGasLimit()).isEqualTo(0x2fefd8);
    assertThat(header.getGasUsed()).isZero();
    assertThat(header.getNumber()).isZero();
    assertThat(header.getReceiptsRoot())
        .isEqualTo(
            Hash.fromHexString(
                "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"));
    assertThat(header.getTransactionsRoot()).isEqualTo(Hash.EMPTY_TRIE_HASH);
    assertThat(header.getOmmersHash())
        .isEqualTo(
            Hash.fromHexString(
                "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347"));
    assertThat(header.getExtraData()).isEqualTo(Bytes.EMPTY);
    assertThat(header.getParentHash()).isEqualTo(Hash.ZERO);

    final MutableWorldState worldState = InMemoryKeyValueStorageProvider.createInMemoryWorldState();
    genesisState.writeStateTo(worldState);
    Hash computedStateRoot = worldState.rootHash();
    assertThat(computedStateRoot).isEqualTo(header.getStateRoot());
    assertThat(header.getStateRoot())
        .isEqualTo(
            Hash.fromHexString(
                "0x7f5cfe1375a61009a22d24512d18035bc8f855129452fa9c6a6be2ef4e9da7db"));
    final Account first =
        worldState.get(Address.fromHexString("0000000000000000000000000000000000000100"));
    final Account last =
        worldState.get(Address.fromHexString("fb289e2b2b65fb63299a682d000744671c50417b"));
    assertThat(first).isNotNull();
    assertThat(first.getBalance().toLong()).isZero();
    assertThat(first.getCode())
        .isEqualTo(Bytes.fromHexString("0x5f804955600180495560028049556003804955"));
    assertThat(last).isNotNull();
    Wei lastBalance = last.getBalance();
    assertThat(lastBalance).isEqualTo(Wei.fromHexString("0x123450000000000000000"));

    assertThat(header.getRequestsHash().isPresent()).isTrue();
    assertThat(header.getRequestsHash().get())
        .isEqualTo(
            Hash.fromHexString(
                "0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"));
  }

  @Test
  void dryRunDetector() {
    assertThat(true)
        .withFailMessage("This test is here so gradle --dry-run executes this class")
        .isTrue();
  }
}
