@BeforeClass
 public static void initNetwork() {
   NetworkInfos.setDefault(NETWORK_INFO);
 }
 @AfterClass
 public static void resetNetwork() {
   NetworkInfos.setDefault(null);
 }
@RunWith(Enclosed.class)
public class NemesisBlockMainnetTest {
  private static final NetworkInfo NETWORK_INFO = NetworkInfos.getMainNetworkInfo();
  private static final NemesisBlockInfo NEMESIS_BLOCK_INFO = NETWORK_INFO.getNemesisBlockInfo();
  // users, devs, marketing, contributors + funds (transfer + multisig)
  private static final int NUM_NEMESIS_TRANSFER_TRANSACTIONS = 1307 + 21 + 5 + 8 + 6;
  private static final int NUM_NEMESIS_TRANSACTIONS = NUM_NEMESIS_TRANSFER_TRANSACTIONS + 6;
  private static final Amount EXPECTED_MULTISIG_AGGREGATE_FEE =
      Amount.fromNem(2 * (5 + 3 * 4) + 2 * (5 + 3 * 5) + 2 * (5 + 3 * 6));
  private static final int EXPECTED_VERSION = 0x68000001;

  @BeforeClass
  public static void initNetwork() {
    NetworkInfos.setDefault(NETWORK_INFO);
  }

  @AfterClass
  public static void resetNetwork() {
    NetworkInfos.setDefault(null);
  }

  private abstract static class AbstractNemesisBlockTest {

    // basic

    @Test
    public void nemesisBlockCanBeCreated() {
      // Act:
      final Block block = this.loadNemesisBlock();

      // Assert:
      Assert.assertThat(
          block.getSigner().getAddress(), IsEqual.equalTo(NEMESIS_BLOCK_INFO.getAddress()));
      Assert.assertThat(block.getType(), IsEqual.equalTo(-1));
      Assert.assertThat(block.getVersion(), IsEqual.equalTo(EXPECTED_VERSION));
      Assert.assertThat(block.getTimeStamp(), IsEqual.equalTo(TimeInstant.ZERO));

      // 2 multisig aggregate transactions
      Assert.assertThat(
          block.getTotalFee(), IsEqual.equalTo(EXPECTED_MULTISIG_AGGREGATE_FEE.multiply(2)));
      Assert.assertThat(block.getPreviousBlockHash(), IsEqual.equalTo(Hash.ZERO));
      Assert.assertThat(block.getHeight(), IsEqual.equalTo(BlockHeight.ONE));
      Assert.assertThat(block.getTransactions().size(), IsEqual.equalTo(NUM_NEMESIS_TRANSACTIONS));

      Assert.assertThat(block.getDifficulty(), IsEqual.equalTo(BlockDifficulty.INITIAL_DIFFICULTY));
      Assert.assertThat(block.getGenerationHash(), IsNull.notNullValue());
    }

    @Test
    public void nemesisBlockIsVerifiable() {
      // Arrange:
      final Block block = this.loadNemesisBlock();

      // Assert:
      Assert.assertThat(block.verify(), IsEqual.equalTo(true));
    }

    @Test
    public void nemesisTransactionsAreVerifiable() {
      // Arrange:
      final Block block = this.loadNemesisBlock();

      // Assert:
      for (final Transaction transaction : block.getTransactions()) {
        Assert.assertThat(transaction.verify(), IsEqual.equalTo(true));
      }
    }

    @Test
    public void nemesisTransactionsHaveCorrectFees() {
      // Arrange:
      final Block block = this.loadNemesisBlock();

      // Assert:
      for (final Transaction transaction : block.getTransactions()) {
        final Amount expectedFee =
            TransactionTypes.TRANSFER == transaction.getType()
                ? Amount.ZERO
                : NemGlobals.getTransactionFeeCalculator().calculateMinimumFee(transaction);
        Assert.assertThat(transaction.getFee(), IsEqual.equalTo(expectedFee));
      }
    }

    @Test
    public void nemesisAddressesAreValid() {
      // Arrange:
      final Block block = this.loadNemesisBlock();

      // Act:
      final Set<Address> allAddresses =
          block
              .getTransactions()
              .stream()
              .flatMap(t -> t.getAccounts().stream().map(Account::getAddress))
              .collect(Collectors.toSet());

      // Assert:
      for (final Address address : allAddresses) {
        Assert.assertThat(address.toString(), address.isValid(), IsEqual.equalTo(true));
      }
    }

    @Test
    public void nemesisTransactionSignersHavePublicKeys() {
      // Arrange:
      final Block block = this.loadNemesisBlock();

      // Act:
      final Set<Address> signerAddresses =
          block
              .getTransactions()
              .stream()
              .map(t -> t.getSigner().getAddress())
              .collect(Collectors.toSet());

      // Assert:
      for (final Address address : signerAddresses) {
        Assert.assertThat(address.getPublicKey(), IsNull.notNullValue());
      }
    }

    // endregion

    // region constants

    @Test
    public void amountConstantIsConsistentWithNemesisBlock() {
      // Act:
      Amount totalAmount = Amount.ZERO;
      final Block block = this.loadNemesisBlock();
      for (final Transaction transaction : block.getTransactions()) {
        if (transaction instanceof TransferTransaction) {
          totalAmount = totalAmount.add(((TransferTransaction) transaction).getAmount());
        }
      }

      // Assert:
      Assert.assertThat(totalAmount, IsEqual.equalTo(NEMESIS_BLOCK_INFO.getAmount()));
    }

    @Test
    public void addressConstantIsConsistentWithNemesisBlock() {
      // Arrange:
      final Block block = this.loadNemesisBlock();
      final Address blockAddress = block.getSigner().getAddress();

      // Assert:
      Assert.assertThat(blockAddress, IsEqual.equalTo(NEMESIS_BLOCK_INFO.getAddress()));
      Assert.assertThat(
          blockAddress.getPublicKey(),
          IsEqual.equalTo(NEMESIS_BLOCK_INFO.getAddress().getPublicKey()));
      Assert.assertThat(blockAddress.getPublicKey(), IsNull.notNullValue());
    }

    @Test
    public void generationHashConstantIsConsistentWithNemesisBlock() {
      // Arrange:
      final Block block = this.loadNemesisBlock();

      // Assert:
      Assert.assertThat(
          block.getGenerationHash(), IsEqual.equalTo(NEMESIS_BLOCK_INFO.getGenerationHash()));
    }

    // endregion

    protected abstract Block loadNemesisBlock(final MockAccountLookup accountLookup);

    protected Block loadNemesisBlock() {
      return this.loadNemesisBlock(new MockAccountLookup());
    }
  }

  // region basic

  public static class ResourceNemesisBlockTest extends AbstractNemesisBlockTest {

    @Override
    protected Block loadNemesisBlock(final MockAccountLookup accountLookup) {
      return NemesisBlock.fromResource(
          NEMESIS_BLOCK_INFO, new DeserializationContext(accountLookup));
    }
  }

  public static class BinaryNemesisBlockTest extends AbstractNemesisBlockTest {

    @Test
    public void nemesisBlockCannotBeLoadedFromBlobWithIncorrectType() {
      // Arrange (set type to 1):
      final byte[] buffer = loadNemesisBlockBlobObject();
      buffer[0] = 1;
      buffer[1] = 0;
      buffer[2] = 0;
      buffer[3] = 0;

      // Act:
      ExceptionAssert.assertThrows(
          v ->
              NemesisBlock.fromBlobObject(
                  NEMESIS_BLOCK_INFO, buffer, new DeserializationContext(new MockAccountLookup())),
          IllegalArgumentException.class);
    }

    @Test
    public void nemesisBlockCannotBeLoadedFromInvalidBlob() {
      // Arrange:
      final byte[] buffer = loadNemesisBlockBlobObject();
      final byte[] badBuffer1 =
          ByteBuffer.allocate(3 + buffer.length).put("bad".getBytes()).put(buffer).array();
      final byte[] badBuffer2 =
          ByteBuffer.allocate(3 + buffer.length)
              .put(Arrays.copyOfRange(buffer, 0, 100))
              .put("bad".getBytes())
              .put(Arrays.copyOfRange(buffer, 100, buffer.length))
              .array();

      // Act:
      ExceptionAssert.assertThrows(
          v ->
              NemesisBlock.fromBlobObject(
                  NEMESIS_BLOCK_INFO,
                  badBuffer1,
                  new DeserializationContext(new MockAccountLookup())),
          IllegalArgumentException.class);
      ExceptionAssert.assertThrows(
          v ->
              NemesisBlock.fromBlobObject(
                  NEMESIS_BLOCK_INFO,
                  badBuffer2,
                  new DeserializationContext(new MockAccountLookup())),
          SerializationException.class);
    }

    @Override
    protected Block loadNemesisBlock(final MockAccountLookup accountLookup) {
      final byte[] blob = loadNemesisBlockBlobObject();
      return NemesisBlock.fromBlobObject(
          NEMESIS_BLOCK_INFO, blob, new DeserializationContext(accountLookup));
    }

    private static byte[] loadNemesisBlockBlobObject() {
      try (final InputStream fin =
          NemesisBlock.class
              .getClassLoader()
              .getResourceAsStream(NEMESIS_BLOCK_INFO.getDataFileName())) {
        return IOUtils.toByteArray(fin);
      } catch (final IOException e) {
        throw new IllegalStateException(
            "unexpected exception was thrown when parsing nemesis block resource");
      }
    }
  }
}