/*
 * 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.consensus.ibft;

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

import org.hyperledger.besu.consensus.common.VoteType;
import org.hyperledger.besu.crypto.SECP256K1.Signature;
import org.hyperledger.besu.ethereum.core.Address;
import org.hyperledger.besu.ethereum.core.BlockHeader;
import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput;
import org.hyperledger.besu.ethereum.rlp.RLPException;
import org.hyperledger.besu.ethereum.rlp.RLPInput;

import java.math.BigInteger;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.Random;

import com.google.common.collect.Lists;
import org.apache.tuweni.bytes.Bytes;
import org.junit.Test;

public class IbftExtraDataTest {

  private final String RAW_HEX_ENCODING_STRING =
      "f8f1a00102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20ea9400000000000000000000000000000000000"
          + "00001940000000000000000000000000000000000000002d794000000000000000000000000000000000000000181ff8400fedc"
          + "baf886b841000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000"
          + "0000000000000000000000000000000000a00b84100000000000000000000000000000000000000000000000000000000000000"
          + "0a000000000000000000000000000000000000000000000000000000000000000100";

  private final IbftExtraData DECODED_EXTRA_DATA_FOR_RAW_HEX_ENCODING_STRING =
      getDecodedExtraDataForRawHexEncodingString();

  private static IbftExtraData getDecodedExtraDataForRawHexEncodingString() {
    final List<Address> validators =
        Arrays.asList(Address.fromHexString("1"), Address.fromHexString("2"));
    final Optional<Vote> vote = Optional.of(Vote.authVote(Address.fromHexString("1")));
    final int round = 0x00FEDCBA;
    final List<Signature> committerSeals =
        Arrays.asList(
            Signature.create(BigInteger.ONE, BigInteger.TEN, (byte) 0),
            Signature.create(BigInteger.TEN, BigInteger.ONE, (byte) 0));

    // Create a byte buffer with no data.
    final byte[] vanity_bytes = createNonEmptyVanityData();
    final Bytes vanity_data = Bytes.wrap(vanity_bytes);

    return new IbftExtraData(vanity_data, committerSeals, vote, round, validators);
  }

  @Test
  public void correctlyCodedRoundConstitutesValidContent() {
    final List<Address> validators = Lists.newArrayList();
    final Optional<Vote> vote = Optional.of(Vote.authVote(Address.fromHexString("1")));
    final int round = 0x00FEDCBA;
    final byte[] roundAsByteArray = new byte[] {(byte) 0x00, (byte) 0xFE, (byte) 0xDC, (byte) 0xBA};
    final List<Signature> committerSeals = Lists.newArrayList();

    // Create a byte buffer with no data.
    final byte[] vanity_bytes = new byte[32];
    final Bytes vanity_data = Bytes.wrap(vanity_bytes);

    final BytesValueRLPOutput encoder = new BytesValueRLPOutput();
    encoder.startList();
    encoder.writeBytes(vanity_data);
    encoder.writeList(validators, (validator, rlp) -> rlp.writeBytes(validator));

    // encoded vote
    vote.get().writeTo(encoder);

    // This is to verify that the decoding works correctly when the round is encoded as 4 bytes
    encoder.writeBytes(Bytes.wrap(roundAsByteArray));
    encoder.writeList(committerSeals, (committer, rlp) -> rlp.writeBytes(committer.encodedBytes()));
    encoder.endList();

    final Bytes bufferToInject = encoder.encoded();

    final IbftExtraData extraData = IbftExtraData.decodeRaw(bufferToInject);

    assertThat(extraData.getVanityData()).isEqualTo(vanity_data);
    assertThat(extraData.getRound()).isEqualTo(round);
    assertThat(extraData.getSeals()).isEqualTo(committerSeals);
    assertThat(extraData.getValidators()).isEqualTo(validators);
  }

  /**
   * This test specifically verifies that {@link IbftExtraData#decode(BlockHeader)} uses {@link
   * RLPInput#readInt()} rather than {@link RLPInput#readIntScalar()} to decode the round number
   */
  @Test
  public void incorrectlyEncodedRoundThrowsRlpException() {
    final List<Address> validators = Lists.newArrayList();
    final Optional<Vote> vote = Optional.of(Vote.authVote(Address.fromHexString("1")));
    final byte[] roundAsByteArray = new byte[] {(byte) 0xFE, (byte) 0xDC, (byte) 0xBA};
    final List<Signature> committerSeals = Lists.newArrayList();

    // Create a byte buffer with no data.
    final byte[] vanity_bytes = new byte[32];
    final Bytes vanity_data = Bytes.wrap(vanity_bytes);

    final BytesValueRLPOutput encoder = new BytesValueRLPOutput();
    encoder.startList();
    encoder.writeBytes(vanity_data);
    encoder.writeList(validators, (validator, rlp) -> rlp.writeBytes(validator));

    // encoded vote
    vote.get().writeTo(encoder);

    // This is to verify that the decoding throws an exception when the round number is not encoded
    // in 4 byte format
    encoder.writeBytes(Bytes.wrap(roundAsByteArray));
    encoder.writeList(committerSeals, (committer, rlp) -> rlp.writeBytes(committer.encodedBytes()));
    encoder.endList();

    final Bytes bufferToInject = encoder.encoded();

    assertThatThrownBy(() -> IbftExtraData.decodeRaw(bufferToInject))
        .isInstanceOf(RLPException.class);
  }

  @Test
  public void nullVoteAndListConstituteValidContent() {
    final List<Address> validators = Lists.newArrayList();
    final int round = 0x00FEDCBA;
    final List<Signature> committerSeals = Lists.newArrayList();

    // Create a byte buffer with no data.
    final byte[] vanity_bytes = new byte[32];
    final Bytes vanity_data = Bytes.wrap(vanity_bytes);

    final BytesValueRLPOutput encoder = new BytesValueRLPOutput();
    encoder.startList();
    encoder.writeBytes(vanity_data);
    encoder.writeList(validators, (validator, rlp) -> rlp.writeBytes(validator));

    // encode an empty vote
    encoder.writeNull();

    encoder.writeInt(round);
    encoder.writeList(committerSeals, (committer, rlp) -> rlp.writeBytes(committer.encodedBytes()));
    encoder.endList();

    final Bytes bufferToInject = encoder.encoded();

    final IbftExtraData extraData = IbftExtraData.decodeRaw(bufferToInject);

    assertThat(extraData.getVanityData()).isEqualTo(vanity_data);
    assertThat(extraData.getVote().isPresent()).isEqualTo(false);
    assertThat(extraData.getRound()).isEqualTo(round);
    assertThat(extraData.getSeals()).isEqualTo(committerSeals);
    assertThat(extraData.getValidators()).isEqualTo(validators);
  }

  @Test
  public void emptyVoteAndListIsEncodedCorrectly() {
    final List<Address> validators = Lists.newArrayList();
    final Optional<Vote> vote = Optional.empty();
    final int round = 0x00FEDCBA;
    final List<Signature> committerSeals = Lists.newArrayList();

    // Create a byte buffer with no data.
    final byte[] vanity_bytes = new byte[32];
    final Bytes vanity_data = Bytes.wrap(vanity_bytes);

    IbftExtraData expectedExtraData =
        new IbftExtraData(vanity_data, committerSeals, vote, round, validators);

    IbftExtraData actualExtraData = IbftExtraData.decodeRaw(expectedExtraData.encode());

    assertThat(actualExtraData).isEqualToComparingFieldByField(expectedExtraData);
  }

  @Test
  public void emptyListConstituteValidContent() {
    final List<Address> validators = Lists.newArrayList();
    final Optional<Vote> vote = Optional.of(Vote.dropVote(Address.fromHexString("1")));
    final int round = 0x00FEDCBA;
    final List<Signature> committerSeals = Lists.newArrayList();

    // Create a byte buffer with no data.
    final byte[] vanity_bytes = new byte[32];
    final Bytes vanity_data = Bytes.wrap(vanity_bytes);

    final BytesValueRLPOutput encoder = new BytesValueRLPOutput();
    encoder.startList();
    encoder.writeBytes(vanity_data);
    encoder.writeList(validators, (validator, rlp) -> rlp.writeBytes(validator));

    // encoded vote
    vote.get().writeTo(encoder);

    encoder.writeInt(round);
    encoder.writeList(committerSeals, (committer, rlp) -> rlp.writeBytes(committer.encodedBytes()));
    encoder.endList();

    final Bytes bufferToInject = encoder.encoded();

    final IbftExtraData extraData = IbftExtraData.decodeRaw(bufferToInject);

    assertThat(extraData.getVanityData()).isEqualTo(vanity_data);
    assertThat(extraData.getRound()).isEqualTo(round);
    assertThat(extraData.getSeals()).isEqualTo(committerSeals);
    assertThat(extraData.getValidators()).isEqualTo(validators);
  }

  @Test
  public void emptyListsAreEncodedAndDecodedCorrectly() {
    final List<Address> validators = Lists.newArrayList();
    final Optional<Vote> vote = Optional.of(Vote.authVote(Address.fromHexString("1")));
    final int round = 0x00FEDCBA;
    final List<Signature> committerSeals = Lists.newArrayList();

    // Create a byte buffer with no data.
    final byte[] vanity_bytes = new byte[32];
    final Bytes vanity_data = Bytes.wrap(vanity_bytes);

    IbftExtraData expectedExtraData =
        new IbftExtraData(vanity_data, committerSeals, vote, round, validators);

    IbftExtraData actualExtraData = IbftExtraData.decodeRaw(expectedExtraData.encode());

    assertThat(actualExtraData).isEqualToComparingFieldByField(expectedExtraData);
  }

  @Test
  public void fullyPopulatedDataProducesCorrectlyFormedExtraDataObject() {
    final List<Address> validators =
        Arrays.asList(Address.fromHexString("1"), Address.fromHexString("2"));
    final int round = 0x00FEDCBA;
    final List<Signature> committerSeals =
        Arrays.asList(
            Signature.create(BigInteger.ONE, BigInteger.TEN, (byte) 0),
            Signature.create(BigInteger.TEN, BigInteger.ONE, (byte) 0));

    // Create randomised vanity data.
    final byte[] vanity_bytes = createNonEmptyVanityData();
    new Random().nextBytes(vanity_bytes);
    final Bytes vanity_data = Bytes.wrap(vanity_bytes);

    final BytesValueRLPOutput encoder = new BytesValueRLPOutput();
    encoder.startList(); // This is required to create a "root node" for all RLP'd data
    encoder.writeBytes(vanity_data);
    encoder.writeList(validators, (validator, rlp) -> rlp.writeBytes(validator));

    // encoded vote
    encoder.startList();
    encoder.writeBytes(Address.fromHexString("1"));
    encoder.writeByte(Vote.ADD_BYTE_VALUE);
    encoder.endList();

    encoder.writeInt(round);
    encoder.writeList(committerSeals, (committer, rlp) -> rlp.writeBytes(committer.encodedBytes()));
    encoder.endList();

    final Bytes bufferToInject = encoder.encoded();

    final IbftExtraData extraData = IbftExtraData.decodeRaw(bufferToInject);

    assertThat(extraData.getVanityData()).isEqualTo(vanity_data);
    assertThat(extraData.getVote())
        .isEqualTo(Optional.of(new Vote(Address.fromHexString("1"), VoteType.ADD)));
    assertThat(extraData.getRound()).isEqualTo(round);
    assertThat(extraData.getSeals()).isEqualTo(committerSeals);
    assertThat(extraData.getValidators()).isEqualTo(validators);
  }

  @Test
  public void fullyPopulatedDataIsEncodedAndDecodedCorrectly() {
    final List<Address> validators =
        Arrays.asList(Address.fromHexString("1"), Address.fromHexString("2"));
    final Optional<Vote> vote = Optional.of(Vote.authVote(Address.fromHexString("1")));
    final int round = 0x00FEDCBA;
    final List<Signature> committerSeals =
        Arrays.asList(
            Signature.create(BigInteger.ONE, BigInteger.TEN, (byte) 0),
            Signature.create(BigInteger.TEN, BigInteger.ONE, (byte) 0));

    // Create a byte buffer with no data.
    final byte[] vanity_bytes = createNonEmptyVanityData();
    final Bytes vanity_data = Bytes.wrap(vanity_bytes);

    IbftExtraData expectedExtraData =
        new IbftExtraData(vanity_data, committerSeals, vote, round, validators);

    IbftExtraData actualExtraData = IbftExtraData.decodeRaw(expectedExtraData.encode());

    assertThat(actualExtraData).isEqualToComparingFieldByField(expectedExtraData);
  }

  @Test
  public void encodingMatchesKnownRawHexString() {
    final Bytes expectedRawDecoding = Bytes.fromHexString(RAW_HEX_ENCODING_STRING);
    assertThat(DECODED_EXTRA_DATA_FOR_RAW_HEX_ENCODING_STRING.encode())
        .isEqualTo(expectedRawDecoding);
  }

  @Test
  public void decodingOfKnownRawHexStringMatchesKnowExtraDataObject() {

    final IbftExtraData expectedExtraData = DECODED_EXTRA_DATA_FOR_RAW_HEX_ENCODING_STRING;

    Bytes rawDecoding = Bytes.fromHexString(RAW_HEX_ENCODING_STRING);
    IbftExtraData actualExtraData = IbftExtraData.decodeRaw(rawDecoding);

    assertThat(actualExtraData).isEqualToComparingFieldByField(expectedExtraData);
  }

  @Test
  public void extraDataCanBeEncodedWithoutCommitSeals() {
    final List<Address> validators =
        Arrays.asList(Address.fromHexString("1"), Address.fromHexString("2"));
    final Optional<Vote> vote = Optional.of(Vote.authVote(Address.fromHexString("1")));
    final int round = 0x00FEDCBA;
    final List<Signature> committerSeals =
        Arrays.asList(
            Signature.create(BigInteger.ONE, BigInteger.TEN, (byte) 0),
            Signature.create(BigInteger.TEN, BigInteger.ONE, (byte) 0));

    // Create a byte buffer with no data.
    final byte[] vanity_bytes = createNonEmptyVanityData();
    final Bytes vanity_data = Bytes.wrap(vanity_bytes);

    final BytesValueRLPOutput encoder = new BytesValueRLPOutput();
    encoder.startList();
    encoder.writeBytes(vanity_data);
    encoder.writeList(validators, (validator, rlp) -> rlp.writeBytes(validator));

    // encoded vote
    vote.get().writeTo(encoder);

    encoder.writeInt(round);
    encoder.endList();

    Bytes expectedEncoding = encoder.encoded();

    Bytes actualEncoding =
        new IbftExtraData(vanity_data, committerSeals, vote, round, validators)
            .encodeWithoutCommitSeals();

    assertThat(actualEncoding).isEqualTo(expectedEncoding);
  }

  @Test
  public void extraDataCanBeEncodedwithoutCommitSealsOrRoundNumber() {
    final List<Address> validators =
        Arrays.asList(Address.fromHexString("1"), Address.fromHexString("2"));
    final Optional<Vote> vote = Optional.of(Vote.authVote(Address.fromHexString("1")));
    final int round = 0x00FEDCBA;
    final List<Signature> committerSeals =
        Arrays.asList(
            Signature.create(BigInteger.ONE, BigInteger.TEN, (byte) 0),
            Signature.create(BigInteger.TEN, BigInteger.ONE, (byte) 0));

    // Create a byte buffer with no data.
    final byte[] vanity_bytes = createNonEmptyVanityData();
    final Bytes vanity_data = Bytes.wrap(vanity_bytes);

    final BytesValueRLPOutput encoder = new BytesValueRLPOutput();
    encoder.startList();
    encoder.writeBytes(vanity_data);
    encoder.writeList(validators, (validator, rlp) -> rlp.writeBytes(validator));

    // encoded vote
    vote.get().writeTo(encoder);

    encoder.endList();

    Bytes expectedEncoding = encoder.encoded();

    Bytes actualEncoding =
        new IbftExtraData(vanity_data, committerSeals, vote, round, validators)
            .encodeWithoutCommitSealsAndRoundNumber();

    assertThat(actualEncoding).isEqualTo(expectedEncoding);
  }

  @Test
  public void incorrectlyStructuredRlpThrowsException() {
    final List<Address> validators = Lists.newArrayList();
    final Optional<Vote> vote = Optional.of(Vote.authVote(Address.fromHexString("1")));
    final int round = 0x00FEDCBA;
    final List<Signature> committerSeals = Lists.newArrayList();

    // Create a byte buffer with no data.
    final byte[] vanity_bytes = new byte[32];
    final Bytes vanity_data = Bytes.wrap(vanity_bytes);

    final BytesValueRLPOutput encoder = new BytesValueRLPOutput();
    encoder.startList();
    encoder.writeBytes(vanity_data);
    encoder.writeList(validators, (validator, rlp) -> rlp.writeBytes(validator));

    // encoded vote
    vote.get().writeTo(encoder);

    encoder.writeInt(round);
    encoder.writeList(committerSeals, (committer, rlp) -> rlp.writeBytes(committer.encodedBytes()));
    encoder.writeLong(1);
    encoder.endList();

    final Bytes bufferToInject = encoder.encoded();

    assertThatThrownBy(() -> IbftExtraData.decodeRaw(bufferToInject))
        .isInstanceOf(RLPException.class);
  }

  @Test
  public void incorrectVoteTypeThrowsException() {
    final List<Address> validators =
        Arrays.asList(Address.fromHexString("1"), Address.fromHexString("2"));
    final Address voteRecipient = Address.fromHexString("1");
    final byte voteType = (byte) 0xAA;
    final int round = 0x00FEDCBA;
    final List<Signature> committerSeals =
        Arrays.asList(
            Signature.create(BigInteger.ONE, BigInteger.TEN, (byte) 0),
            Signature.create(BigInteger.TEN, BigInteger.ONE, (byte) 0));

    // Create a byte buffer with no data.
    final byte[] vanity_bytes = new byte[32];
    final Bytes vanity_data = Bytes.wrap(vanity_bytes);

    final BytesValueRLPOutput encoder = new BytesValueRLPOutput();
    encoder.startList();
    encoder.writeBytes(vanity_data);
    encoder.writeList(validators, (validator, rlp) -> rlp.writeBytes(validator));

    // encode vote
    encoder.startList();
    encoder.writeBytes(voteRecipient);
    encoder.writeByte(voteType);
    encoder.endList();

    encoder.writeInt(round);
    encoder.writeList(committerSeals, (committer, rlp) -> rlp.writeBytes(committer.encodedBytes()));
    encoder.endList();

    final Bytes bufferToInject = encoder.encoded();

    assertThatThrownBy(() -> IbftExtraData.decodeRaw(bufferToInject))
        .isInstanceOf(RLPException.class);
  }

  @Test
  public void emptyExtraDataThrowsException() {
    final Bytes bufferToInject = Bytes.EMPTY;

    assertThatThrownBy(() -> IbftExtraData.decodeRaw(bufferToInject))
        .isInstanceOf(IllegalArgumentException.class)
        .hasMessage("Invalid Bytes supplied - Ibft Extra Data required.");
  }

  private static byte[] createNonEmptyVanityData() {
    final byte[] vanity_bytes = new byte[32];
    for (int i = 0; i < vanity_bytes.length; i++) {
      vanity_bytes[i] = (byte) (i + 1);
    }
    return vanity_bytes;
  }
}