/*
 * 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.ethereum.core.Account;
import org.hyperledger.besu.ethereum.core.Address;
import org.hyperledger.besu.ethereum.core.BlockHeader;
import org.hyperledger.besu.ethereum.core.Hash;
import org.hyperledger.besu.ethereum.core.InMemoryStorageProvider;
import org.hyperledger.besu.ethereum.core.MutableWorldState;
import org.hyperledger.besu.ethereum.mainnet.MainnetProtocolSchedule;
import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput;

import com.google.common.base.Charsets;
import com.google.common.io.Resources;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.units.bigints.UInt256;
import org.bouncycastle.util.encoders.Hex;
import org.junit.Test;

public 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 =
      "[encoded data, 11402 characters]";

  @Test
  public void createFromJsonWithAllocs() throws Exception {
    final GenesisState genesisState =
        GenesisState.fromJson(
            Resources.toString(GenesisStateTest.class.getResource("genesis1.json"), Charsets.UTF_8),
            MainnetProtocolSchedule.create());
    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 = InMemoryStorageProvider.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);
  }

  @Test
  public void createFromJsonNoAllocs() throws Exception {
    final GenesisState genesisState =
        GenesisState.fromJson(
            Resources.toString(GenesisStateTest.class.getResource("genesis2.json"), Charsets.UTF_8),
            MainnetProtocolSchedule.create());
    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 String sourceFile, final String blockHash, final int version) throws Exception {
    final GenesisState genesisState =
        GenesisState.fromJson(
            Resources.toString(GenesisStateTest.class.getResource(sourceFile), Charsets.UTF_8),
            MainnetProtocolSchedule.create());
    final BlockHeader header = genesisState.getBlock().getHeader();
    assertThat(header.getHash()).isEqualTo(Hash.fromHexString(blockHash));

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

  @Test
  public void createFromJsonWithContract() throws Exception {
    assertContractInvariants(
        "genesis3.json", "0xe7fd8db206dcaf066b7c97b8a42a0abc18653613560748557ab44868652a78b6", 0);
  }

  @Test
  public void createFromJsonWithVersion() throws Exception {
    assertContractInvariants(
        "genesis4.json", "0x3224ddae856381f5fb67492b4561ecbc0cb1e9e50e6cf3238f6e049fe95a8604", 1);
  }

  @Test
  public void createFromJsonWithNonce() throws Exception {
    final GenesisState genesisState =
        GenesisState.fromJson(
            Resources.toString(
                GenesisStateTest.class.getResource("genesisNonce.json"), Charsets.UTF_8),
            MainnetProtocolSchedule.create());
    final BlockHeader header = genesisState.getBlock().getHeader();
    assertThat(header.getHash())
        .isEqualTo(
            Hash.fromHexString(
                "0x36750291f1a8429aeb553a790dc2d149d04dbba0ca4cfc7fd5eb12d478117c9f"));
  }

  @Test
  public void encodeOlympicBlock() throws Exception {
    final GenesisState genesisState =
        GenesisState.fromJson(
            Resources.toString(
                GenesisStateTest.class.getResource("genesis-olympic.json"), Charsets.UTF_8),
            MainnetProtocolSchedule.create());
    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));
  }
}
