/*
 * 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.eth.manager;

import static com.google.common.base.Preconditions.checkArgument;
import static org.hyperledger.besu.ethereum.core.InMemoryKeyValueStorageProvider.createInMemoryBlockchain;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;

import org.hyperledger.besu.config.GenesisConfigFile;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.ethereum.chain.Blockchain;
import org.hyperledger.besu.ethereum.chain.ChainHead;
import org.hyperledger.besu.ethereum.chain.GenesisState;
import org.hyperledger.besu.ethereum.core.BlockHeader;
import org.hyperledger.besu.ethereum.core.BlockchainSetupUtil;
import org.hyperledger.besu.ethereum.core.Difficulty;
import org.hyperledger.besu.ethereum.core.ProtocolScheduleFixture;
import org.hyperledger.besu.ethereum.eth.EthProtocol;
import org.hyperledger.besu.ethereum.eth.EthProtocolConfiguration;
import org.hyperledger.besu.ethereum.eth.manager.snap.SnapProtocolManager;
import org.hyperledger.besu.ethereum.eth.peervalidation.PeerValidator;
import org.hyperledger.besu.ethereum.eth.sync.ChainHeadTracker;
import org.hyperledger.besu.ethereum.eth.sync.SyncMode;
import org.hyperledger.besu.ethereum.eth.sync.SynchronizerConfiguration;
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool;
import org.hyperledger.besu.ethereum.forkid.ForkIdManager;
import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule;
import org.hyperledger.besu.ethereum.p2p.rlpx.wire.DefaultMessage;
import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData;
import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive;
import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem;
import org.hyperledger.besu.plugin.services.storage.DataStorageFormat;
import org.hyperledger.besu.testutil.DeterministicEthScheduler;
import org.hyperledger.besu.testutil.DeterministicEthScheduler.TimeoutPolicy;
import org.hyperledger.besu.testutil.TestClock;

import java.math.BigInteger;
import java.util.Collections;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.concurrent.CompletableFuture;

import org.apache.tuweni.bytes.Bytes;
import org.mockito.Mockito;

public class EthProtocolManagerTestUtil {

  public static EthProtocolManager create(
      final ProtocolSchedule protocolSchedule,
      final Blockchain blockchain,
      final TimeoutPolicy timeoutPolicy,
      final WorldStateArchive worldStateArchive,
      final TransactionPool transactionPool,
      final EthProtocolConfiguration ethereumWireProtocolConfiguration) {
    return create(
        protocolSchedule,
        blockchain,
        new DeterministicEthScheduler(timeoutPolicy),
        worldStateArchive,
        transactionPool,
        ethereumWireProtocolConfiguration);
  }

  public static EthProtocolManager create(
      final ProtocolSchedule protocolSchedule,
      final Blockchain blockchain,
      final WorldStateArchive worldStateArchive,
      final TransactionPool transactionPool,
      final EthProtocolConfiguration ethereumWireProtocolConfiguration,
      final Optional<MergePeerFilter> mergePeerFilter) {

    final EthPeers peers =
        new EthPeers(
            EthProtocol.NAME,
            () -> protocolSchedule.getByBlockHeader(blockchain.getChainHeadHeader()),
            TestClock.fixed(),
            new NoOpMetricsSystem(),
            EthProtocolConfiguration.DEFAULT_MAX_MESSAGE_SIZE,
            Collections.emptyList(),
            Bytes.random(64),
            25,
            25,
            false,
            SyncMode.FAST,
            new ForkIdManager(blockchain, Collections.emptyList(), Collections.emptyList(), false));

    final ChainHeadTracker chainHeadTrackerMock = getChainHeadTrackerMock();
    peers.setChainHeadTracker(chainHeadTrackerMock);

    final EthMessages messages = new EthMessages();
    final EthScheduler ethScheduler = new DeterministicEthScheduler(TimeoutPolicy.NEVER_TIMEOUT);
    final EthContext ethContext = new EthContext(peers, messages, ethScheduler);

    return new EthProtocolManager(
        blockchain,
        BigInteger.ONE,
        worldStateArchive,
        transactionPool,
        ethereumWireProtocolConfiguration,
        peers,
        messages,
        ethContext,
        Collections.emptyList(),
        mergePeerFilter,
        mock(SynchronizerConfiguration.class),
        ethScheduler,
        new ForkIdManager(blockchain, Collections.emptyList(), Collections.emptyList(), false));
  }

  public static EthProtocolManager create(
      final Blockchain blockchain,
      final EthScheduler ethScheduler,
      final WorldStateArchive worldStateArchive,
      final TransactionPool transactionPool,
      final EthProtocolConfiguration ethereumWireProtocolConfiguration,
      final EthPeers ethPeers,
      final EthMessages ethMessages,
      final EthContext ethContext) {
    return create(
        blockchain,
        ethScheduler,
        worldStateArchive,
        transactionPool,
        ethereumWireProtocolConfiguration,
        ethPeers,
        ethMessages,
        ethContext,
        new ForkIdManager(blockchain, Collections.emptyList(), Collections.emptyList(), false));
  }

  public static EthProtocolManager create(
      final Blockchain blockchain,
      final EthScheduler ethScheduler,
      final WorldStateArchive worldStateArchive,
      final TransactionPool transactionPool,
      final EthProtocolConfiguration ethereumWireProtocolConfiguration,
      final EthPeers ethPeers,
      final EthMessages ethMessages,
      final EthContext ethContext,
      final ForkIdManager forkIdManager) {

    ethPeers.setChainHeadTracker(getChainHeadTrackerMock());

    final BigInteger networkId = BigInteger.ONE;
    return new EthProtocolManager(
        blockchain,
        networkId,
        worldStateArchive,
        transactionPool,
        ethereumWireProtocolConfiguration,
        ethPeers,
        ethMessages,
        ethContext,
        Collections.emptyList(),
        Optional.empty(),
        mock(SynchronizerConfiguration.class),
        ethScheduler,
        forkIdManager);
  }

  public static EthProtocolManager create(final Blockchain blockchain) {
    return create(
        ProtocolScheduleFixture.MAINNET,
        blockchain,
        new DeterministicEthScheduler(TimeoutPolicy.NEVER_TIMEOUT));
  }

  public static EthProtocolManager create(
      final ProtocolSchedule protocolSchedule,
      final Blockchain blockchain,
      final WorldStateArchive worldStateArchive,
      final TransactionPool transactionPool,
      final EthProtocolConfiguration ethProtocolConfiguration) {
    return create(
        protocolSchedule,
        blockchain,
        new DeterministicEthScheduler(TimeoutPolicy.NEVER_TIMEOUT),
        worldStateArchive,
        transactionPool,
        ethProtocolConfiguration);
  }

  public static EthProtocolManager create(final EthScheduler ethScheduler) {
    final ProtocolSchedule protocolSchedule = ProtocolScheduleFixture.MAINNET;
    final GenesisConfigFile config = GenesisConfigFile.mainnet();
    final GenesisState genesisState = GenesisState.fromConfig(config, protocolSchedule);
    final Blockchain blockchain = createInMemoryBlockchain(genesisState.getBlock());
    return create(protocolSchedule, blockchain, ethScheduler);
  }

  public static EthProtocolManager create(
      final ProtocolSchedule protocolSchedule,
      final Blockchain blockchain,
      final EthScheduler ethScheduler,
      final WorldStateArchive worldStateArchive,
      final TransactionPool transactionPool,
      final EthProtocolConfiguration configuration) {

    final EthPeers peers =
        new EthPeers(
            EthProtocol.NAME,
            () -> protocolSchedule.getByBlockHeader(blockchain.getChainHeadHeader()),
            TestClock.fixed(),
            new NoOpMetricsSystem(),
            EthProtocolConfiguration.DEFAULT_MAX_MESSAGE_SIZE,
            Collections.emptyList(),
            Bytes.random(64),
            25,
            25,
            false,
            SyncMode.FAST,
            new ForkIdManager(blockchain, Collections.emptyList(), Collections.emptyList(), false));
    final EthMessages messages = new EthMessages();

    final ChainHeadTracker chtMock = getChainHeadTrackerMock();

    peers.setChainHeadTracker(chtMock);

    return create(
        blockchain,
        ethScheduler,
        worldStateArchive,
        transactionPool,
        configuration,
        peers,
        messages,
        new EthContext(peers, messages, ethScheduler));
  }

  public static ChainHeadTracker getChainHeadTrackerMock() {
    final ChainHeadTracker chtMock = mock(ChainHeadTracker.class);
    final BlockHeader blockHeaderMock = mock(BlockHeader.class);
    Mockito.lenient()
        .when(chtMock.getBestHeaderFromPeer(any()))
        .thenReturn(CompletableFuture.completedFuture(blockHeaderMock));
    Mockito.lenient().when(blockHeaderMock.getNumber()).thenReturn(0L);
    Mockito.lenient().when(blockHeaderMock.getStateRoot()).thenReturn(Hash.ZERO);
    return chtMock;
  }

  public static EthProtocolManager create(
      final ProtocolSchedule protocolSchedule,
      final Blockchain blockchain,
      final EthScheduler ethScheduler,
      final WorldStateArchive worldStateArchive,
      final TransactionPool transactionPool,
      final EthProtocolConfiguration configuration,
      final ForkIdManager forkIdManager) {

    final EthPeers peers =
        new EthPeers(
            EthProtocol.NAME,
            () -> protocolSchedule.getByBlockHeader(blockchain.getChainHeadHeader()),
            TestClock.fixed(),
            new NoOpMetricsSystem(),
            EthProtocolConfiguration.DEFAULT_MAX_MESSAGE_SIZE,
            Collections.emptyList(),
            Bytes.random(64),
            25,
            25,
            false,
            SyncMode.FAST,
            new ForkIdManager(blockchain, Collections.emptyList(), Collections.emptyList(), false));
    final EthMessages messages = new EthMessages();

    return create(
        blockchain,
        ethScheduler,
        worldStateArchive,
        transactionPool,
        configuration,
        peers,
        messages,
        new EthContext(peers, messages, ethScheduler),
        forkIdManager);
  }

  public static EthProtocolManager create(
      final ProtocolSchedule protocolSchedule,
      final Blockchain blockchain,
      final EthScheduler ethScheduler) {
    final EthPeers ethPeers =
        new EthPeers(
            EthProtocol.NAME,
            () -> protocolSchedule.getByBlockHeader(blockchain.getChainHeadHeader()),
            TestClock.fixed(),
            new NoOpMetricsSystem(),
            EthProtocolConfiguration.DEFAULT_MAX_MESSAGE_SIZE,
            Collections.emptyList(),
            Bytes.random(64),
            25,
            25,
            false,
            SyncMode.FAST,
            new ForkIdManager(blockchain, Collections.emptyList(), Collections.emptyList(), false));

    final ChainHeadTracker chainHeadTrackerMock = getChainHeadTrackerMock();
    ethPeers.setChainHeadTracker(chainHeadTrackerMock);

    final EthMessages messages = new EthMessages();

    return create(
        blockchain,
        ethScheduler,
        BlockchainSetupUtil.forTesting(DataStorageFormat.FOREST).getWorldArchive(),
        mock(TransactionPool.class),
        EthProtocolConfiguration.defaultConfig(),
        ethPeers,
        messages,
        new EthContext(ethPeers, messages, ethScheduler));
  }

  public static EthProtocolManager create() {
    return create(TimeoutPolicy.NEVER_TIMEOUT);
  }

  public static EthProtocolManager create(final TimeoutPolicy timeoutPolicy) {
    return create(new DeterministicEthScheduler(timeoutPolicy));
  }

  // Utility to prevent scheduler from automatically running submitted tasks
  public static void disableEthSchedulerAutoRun(final EthProtocolManager ethProtocolManager) {
    final EthScheduler scheduler = ethProtocolManager.ethContext().getScheduler();
    checkArgument(
        scheduler instanceof DeterministicEthScheduler,
        "EthProtocolManager must be set up with "
            + DeterministicEthScheduler.class.getSimpleName()
            + " in order to disable auto run.");
    ((DeterministicEthScheduler) scheduler).disableAutoRun();
  }

  // Manually runs any pending tasks submitted to the EthScheduler
  // Works with {@code disableEthSchedulerAutoRun} - tasks will only be pending if
  // autoRun has been disabled.
  public static void runPendingFutures(final EthProtocolManager ethProtocolManager) {
    final EthScheduler scheduler = ethProtocolManager.ethContext().getScheduler();
    checkArgument(
        scheduler instanceof DeterministicEthScheduler,
        "EthProtocolManager must be set up with "
            + DeterministicEthScheduler.class.getSimpleName()
            + " in order to manually run pending futures.");
    ((DeterministicEthScheduler) scheduler).runPendingFutures();
  }

  /**
   * Expires any pending timeouts tracked by {@code DeterministicEthScheduler}
   *
   * @param ethProtocolManager The {@code EthProtocolManager} managing the scheduler holding the
   *     timeouts to be expired.
   */
  public static void expirePendingTimeouts(final EthProtocolManager ethProtocolManager) {
    final EthScheduler scheduler = ethProtocolManager.ethContext().getScheduler();
    checkArgument(
        scheduler instanceof DeterministicEthScheduler,
        "EthProtocolManager must be set up with "
            + DeterministicEthScheduler.class.getSimpleName()
            + " in order to manually expire pending timeouts.");
    ((DeterministicEthScheduler) scheduler).expirePendingTimeouts();
  }

  /**
   * Gets the number of pending tasks submitted to the EthScheduler.
   *
   * <p>Works with {@code disableEthSchedulerAutoRun} - tasks will only be pending if autoRun has
   * been disabled.
   */
  public static long getPendingFuturesCount(final EthProtocolManager ethProtocolManager) {
    final EthScheduler scheduler = ethProtocolManager.ethContext().getScheduler();
    checkArgument(
        scheduler instanceof DeterministicEthScheduler,
        "EthProtocolManager must be set up with "
            + DeterministicEthScheduler.class.getSimpleName()
            + " in order to manually run pending futures.");
    return ((DeterministicEthScheduler) scheduler).getPendingFuturesCount();
  }

  public static void broadcastMessage(
      final EthProtocolManager ethProtocolManager,
      final RespondingEthPeer peer,
      final MessageData message) {
    ethProtocolManager.processMessage(
        EthProtocol.ETH63, new DefaultMessage(peer.getPeerConnection(), message));
  }

  public static RespondingEthPeer.Builder peerBuilder() {
    return RespondingEthPeer.builder();
  }

  public static RespondingEthPeer createPeer(
      final EthProtocolManager ethProtocolManager, final Difficulty td) {
    return RespondingEthPeer.builder()
        .ethProtocolManager(ethProtocolManager)
        .totalDifficulty(td)
        .build();
  }

  public static RespondingEthPeer createPeer(
      final EthProtocolManager ethProtocolManager,
      final Difficulty td,
      final long estimatedHeight) {
    return RespondingEthPeer.builder()
        .ethProtocolManager(ethProtocolManager)
        .totalDifficulty(td)
        .estimatedHeight(estimatedHeight)
        .build();
  }

  public static RespondingEthPeer createPeer(
      final EthProtocolManager ethProtocolManager,
      final Difficulty td,
      final OptionalLong estimatedHeight) {
    return RespondingEthPeer.builder()
        .ethProtocolManager(ethProtocolManager)
        .totalDifficulty(td)
        .estimatedHeight(estimatedHeight)
        .build();
  }

  public static RespondingEthPeer createPeer(
      final EthProtocolManager ethProtocolManager,
      final Difficulty td,
      final OptionalLong estimatedHeight,
      final PeerValidator... validators) {
    return RespondingEthPeer.builder()
        .ethProtocolManager(ethProtocolManager)
        .totalDifficulty(td)
        .estimatedHeight(estimatedHeight)
        .peerValidators(validators)
        .build();
  }

  public static RespondingEthPeer createPeer(final EthProtocolManager ethProtocolManager) {
    return RespondingEthPeer.builder().ethProtocolManager(ethProtocolManager).build();
  }

  public static RespondingEthPeer createPeer(
      final EthProtocolManager ethProtocolManager, final long estimatedHeight) {
    return RespondingEthPeer.builder()
        .ethProtocolManager(ethProtocolManager)
        .estimatedHeight(estimatedHeight)
        .build();
  }

  public static RespondingEthPeer createPeer(
      final EthProtocolManager ethProtocolManager,
      final SnapProtocolManager snapProtocolManager,
      final long estimatedHeight) {
    return RespondingEthPeer.builder()
        .ethProtocolManager(ethProtocolManager)
        .estimatedHeight(estimatedHeight)
        .snapProtocolManager(snapProtocolManager)
        .build();
  }

  public static RespondingEthPeer createPeer(
      final EthProtocolManager ethProtocolManager,
      final long estimatedHeight,
      final PeerValidator... validators) {
    return RespondingEthPeer.builder()
        .ethProtocolManager(ethProtocolManager)
        .estimatedHeight(estimatedHeight)
        .peerValidators(validators)
        .build();
  }

  public static RespondingEthPeer createPeer(
      final EthProtocolManager ethProtocolManager, final Blockchain blockchain) {
    final ChainHead head = blockchain.getChainHead();
    return RespondingEthPeer.builder()
        .ethProtocolManager(ethProtocolManager)
        .totalDifficulty(head.getTotalDifficulty())
        .chainHeadHash(head.getHash())
        .estimatedHeight(blockchain.getChainHeadBlockNumber())
        .build();
  }

  public static RespondingEthPeer createPeer(
      final EthProtocolManager ethProtocolManager,
      final Difficulty td,
      final int estimatedHeight,
      final boolean isServingSnap,
      final boolean addToEthPeers) {
    return RespondingEthPeer.builder()
        .ethProtocolManager(ethProtocolManager)
        .totalDifficulty(td)
        .estimatedHeight(estimatedHeight)
        .isServingSnap(isServingSnap)
        .addToEthPeers(addToEthPeers)
        .build();
  }
}
