//! Generic tests that make use of the (newer) `InteractiveApiTester`
use beacon_chain::{
    chain_config::{DisallowedReOrgOffsets, ReOrgThreshold},
    test_utils::{AttestationStrategy, BlockStrategy, SyncCommitteeStrategy},
    ChainConfig,
};
use beacon_processor::work_reprocessing_queue::ReprocessQueueMessage;
use eth2::types::ProduceBlockV3Response;
use eth2::types::{DepositContractData, StateId};
use execution_layer::{ForkchoiceState, PayloadAttributes};
use http_api::test_utils::InteractiveTester;
use parking_lot::Mutex;
use slot_clock::SlotClock;
use state_processing::{
    per_block_processing::get_expected_withdrawals, state_advance::complete_state_advance,
};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use tree_hash::TreeHash;
use types::{
    Address, Epoch, EthSpec, ExecPayload, ExecutionBlockHash, ForkName, MainnetEthSpec,
    MinimalEthSpec, ProposerPreparationData, Slot,
};

type E = MainnetEthSpec;

// Test that the deposit_contract endpoint returns the correct chain_id and address.
// Regression test for https://github.com/sigp/lighthouse/issues/2657
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn deposit_contract_custom_network() {
    let validator_count = 24;
    let mut spec = E::default_spec();

    // Rinkeby, which we don't use elsewhere.
    spec.deposit_chain_id = 4;
    spec.deposit_network_id = 4;
    // Arbitrary contract address.
    spec.deposit_contract_address = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".parse().unwrap();

    let tester = InteractiveTester::<E>::new(Some(spec.clone()), validator_count).await;
    let client = &tester.client;

    let result = client.get_config_deposit_contract().await.unwrap().data;

    let expected = DepositContractData {
        address: spec.deposit_contract_address,
        chain_id: spec.deposit_chain_id,
    };

    assert_eq!(result, expected);
}

// Test that state lookups by root function correctly for states that are finalized but still
// present in the hot database, and have had their block pruned from fork choice.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn state_by_root_pruned_from_fork_choice() {
    type E = MinimalEthSpec;

    let validator_count = 24;
    let spec = ForkName::latest().make_genesis_spec(E::default_spec());

    let tester = InteractiveTester::<E>::new_with_initializer_and_mutator(
        Some(spec.clone()),
        validator_count,
        Some(Box::new(move |builder| {
            builder
                .deterministic_keypairs(validator_count)
                .fresh_ephemeral_store()
                .chain_config(ChainConfig {
                    epochs_per_migration: 1024,
                    ..ChainConfig::default()
                })
        })),
        None,
    )
    .await;

    let client = &tester.client;
    let harness = &tester.harness;

    // Create some chain depth and finalize beyond fork choice's pruning depth.
    let num_epochs = 8_u64;
    let num_initial = num_epochs * E::slots_per_epoch();
    harness.advance_slot();
    harness
        .extend_chain_with_sync(
            num_initial as usize,
            BlockStrategy::OnCanonicalHead,
            AttestationStrategy::AllValidators,
            SyncCommitteeStrategy::NoValidators,
        )
        .await;

    // Should now be finalized.
    let finalized_epoch = harness.finalized_checkpoint().epoch;
    assert_eq!(finalized_epoch, num_epochs - 2);

    // The split slot should still be at 0.
    assert_eq!(harness.chain.store.get_split_slot(), 0);

    // States that are between the split and the finalized slot should be able to be looked up by
    // state root.
    for slot in 0..finalized_epoch.start_slot(E::slots_per_epoch()).as_u64() {
        let state_root = harness
            .chain
            .state_root_at_slot(Slot::new(slot))
            .unwrap()
            .unwrap();
        let response = client
            .get_debug_beacon_states::<E>(StateId::Root(state_root))
            .await
            .unwrap()
            .unwrap();

        assert!(response.metadata.finalized.unwrap());
        assert!(!response.metadata.execution_optimistic.unwrap());

        let mut state = response.data;
        assert_eq!(state.update_tree_hash_cache().unwrap(), state_root);
    }
}

/// Data structure for tracking fork choice updates received by the mock execution layer.
#[derive(Debug, Default)]
struct ForkChoiceUpdates {
    updates: HashMap<ExecutionBlockHash, Vec<ForkChoiceUpdateMetadata>>,
}

#[derive(Debug, Clone)]
struct ForkChoiceUpdateMetadata {
    received_at: Duration,
    state: ForkchoiceState,
    payload_attributes: Option<PayloadAttributes>,
}

impl ForkChoiceUpdates {
    fn insert(&mut self, update: ForkChoiceUpdateMetadata) {
        self.updates
            .entry(update.state.head_block_hash)
            .or_insert_with(Vec::new)
            .push(update);
    }

    fn contains_update_for(&self, block_hash: ExecutionBlockHash) -> bool {
        self.updates.contains_key(&block_hash)
    }

    /// Find the first fork choice update for `head_block_hash` with payload attributes for a
    /// block proposal at `proposal_timestamp`.
    fn first_update_with_payload_attributes(
        &self,
        head_block_hash: ExecutionBlockHash,
        proposal_timestamp: u64,
    ) -> Option<ForkChoiceUpdateMetadata> {
        self.updates
            .get(&head_block_hash)?
            .iter()
            .find(|update| {
                update
                    .payload_attributes
                    .as_ref()
                    .map_or(false, |payload_attributes| {
                        payload_attributes.timestamp() == proposal_timestamp
                    })
            })
            .cloned()
    }
}

pub struct ReOrgTest {
    head_slot: Slot,
    /// Number of slots between parent block and canonical head.
    parent_distance: u64,
    /// Number of slots between head block and block proposal slot.
    head_distance: u64,
    re_org_threshold: u64,
    max_epochs_since_finalization: u64,
    percent_parent_votes: usize,
    percent_empty_votes: usize,
    percent_head_votes: usize,
    should_re_org: bool,
    misprediction: bool,
    /// Whether to expect withdrawals to change on epoch boundaries.
    expect_withdrawals_change_on_epoch: bool,
    /// Epoch offsets to avoid proposing reorg blocks at.
    disallowed_offsets: Vec<u64>,
}

impl Default for ReOrgTest {
    /// Default config represents a regular easy re-org.
    fn default() -> Self {
        Self {
            head_slot: Slot::new(E::slots_per_epoch() - 2),
            parent_distance: 1,
            head_distance: 1,
            re_org_threshold: 20,
            max_epochs_since_finalization: 2,
            percent_parent_votes: 100,
            percent_empty_votes: 100,
            percent_head_votes: 0,
            should_re_org: true,
            misprediction: false,
            expect_withdrawals_change_on_epoch: false,
            disallowed_offsets: vec![],
        }
    }
}

// Test that the beacon node will try to perform proposer boost re-orgs on late blocks when
// configured.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
pub async fn proposer_boost_re_org_zero_weight() {
    proposer_boost_re_org_test(ReOrgTest::default()).await;
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
pub async fn proposer_boost_re_org_epoch_boundary() {
    proposer_boost_re_org_test(ReOrgTest {
        head_slot: Slot::new(E::slots_per_epoch() - 1),
        should_re_org: false,
        ..Default::default()
    })
    .await;
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
pub async fn proposer_boost_re_org_epoch_boundary_skip1() {
    // Proposing a block on a boundary after a skip will change the set of expected withdrawals
    // sent in the payload attributes.
    proposer_boost_re_org_test(ReOrgTest {
        head_slot: Slot::new(2 * E::slots_per_epoch() - 2),
        head_distance: 2,
        should_re_org: false,
        expect_withdrawals_change_on_epoch: true,
        ..Default::default()
    })
    .await;
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
pub async fn proposer_boost_re_org_epoch_boundary_skip32() {
    // Propose a block at 64 after a whole epoch of skipped slots.
    proposer_boost_re_org_test(ReOrgTest {
        head_slot: Slot::new(E::slots_per_epoch() - 1),
        head_distance: E::slots_per_epoch() + 1,
        should_re_org: false,
        expect_withdrawals_change_on_epoch: true,
        ..Default::default()
    })
    .await;
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
pub async fn proposer_boost_re_org_slot_after_epoch_boundary() {
    proposer_boost_re_org_test(ReOrgTest {
        head_slot: Slot::new(33),
        ..Default::default()
    })
    .await;
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
pub async fn proposer_boost_re_org_bad_ffg() {
    proposer_boost_re_org_test(ReOrgTest {
        head_slot: Slot::new(64 + 22),
        should_re_org: false,
        ..Default::default()
    })
    .await;
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
pub async fn proposer_boost_re_org_no_finality() {
    proposer_boost_re_org_test(ReOrgTest {
        head_slot: Slot::new(96),
        percent_parent_votes: 100,
        percent_empty_votes: 0,
        percent_head_votes: 100,
        should_re_org: false,
        ..Default::default()
    })
    .await;
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
pub async fn proposer_boost_re_org_finality() {
    proposer_boost_re_org_test(ReOrgTest {
        head_slot: Slot::new(129),
        ..Default::default()
    })
    .await;
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
pub async fn proposer_boost_re_org_parent_distance() {
    proposer_boost_re_org_test(ReOrgTest {
        head_slot: Slot::new(E::slots_per_epoch() - 2),
        parent_distance: 2,
        should_re_org: false,
        ..Default::default()
    })
    .await;
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
pub async fn proposer_boost_re_org_head_distance() {
    proposer_boost_re_org_test(ReOrgTest {
        head_slot: Slot::new(E::slots_per_epoch() - 3),
        head_distance: 2,
        should_re_org: false,
        ..Default::default()
    })
    .await;
}

// Check that a re-org at a disallowed offset fails.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
pub async fn proposer_boost_re_org_disallowed_offset() {
    let offset = 4;
    proposer_boost_re_org_test(ReOrgTest {
        head_slot: Slot::new(E::slots_per_epoch() + offset - 1),
        disallowed_offsets: vec![offset],
        should_re_org: false,
        ..Default::default()
    })
    .await;
}

// Check that a re-org at the *only* allowed offset succeeds.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
pub async fn proposer_boost_re_org_disallowed_offset_exact() {
    let offset = 4;
    let disallowed_offsets = (0..E::slots_per_epoch()).filter(|o| *o != offset).collect();
    proposer_boost_re_org_test(ReOrgTest {
        head_slot: Slot::new(E::slots_per_epoch() + offset - 1),
        disallowed_offsets,
        ..Default::default()
    })
    .await;
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
pub async fn proposer_boost_re_org_very_unhealthy() {
    proposer_boost_re_org_test(ReOrgTest {
        head_slot: Slot::new(E::slots_per_epoch() - 1),
        parent_distance: 2,
        head_distance: 2,
        percent_parent_votes: 10,
        percent_empty_votes: 10,
        percent_head_votes: 10,
        should_re_org: false,
        ..Default::default()
    })
    .await;
}

/// The head block is late but still receives 30% of the committee vote, leading to a misprediction.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
pub async fn proposer_boost_re_org_weight_misprediction() {
    proposer_boost_re_org_test(ReOrgTest {
        percent_empty_votes: 70,
        percent_head_votes: 30,
        should_re_org: false,
        misprediction: true,
        ..Default::default()
    })
    .await;
}

/// Run a proposer boost re-org test.
///
/// - `head_slot`: the slot of the canonical head to be reorged
/// - `reorg_threshold`: committee percentage value for reorging
/// - `num_empty_votes`: percentage of comm of attestations for the parent block
/// - `num_head_votes`: number of attestations for the head block
/// - `should_re_org`: whether the proposer should build on the parent rather than the head
pub async fn proposer_boost_re_org_test(
    ReOrgTest {
        head_slot,
        parent_distance,
        head_distance,
        re_org_threshold,
        max_epochs_since_finalization,
        percent_parent_votes,
        percent_empty_votes,
        percent_head_votes,
        should_re_org,
        misprediction,
        expect_withdrawals_change_on_epoch,
        disallowed_offsets,
    }: ReOrgTest,
) {
    assert!(head_slot > 0);

    // Test using the latest fork so that we simulate conditions as similar to mainnet as possible.
    let mut spec = ForkName::latest().make_genesis_spec(E::default_spec());
    spec.terminal_total_difficulty = 1.into();

    // Ensure there are enough validators to have `attesters_per_slot`.
    let attesters_per_slot = 10;
    let validator_count = E::slots_per_epoch() as usize * attesters_per_slot;
    let all_validators = (0..validator_count).collect::<Vec<usize>>();
    let num_initial = head_slot.as_u64().checked_sub(parent_distance + 1).unwrap();

    // Check that the required vote percentages can be satisfied exactly using `attesters_per_slot`.
    assert_eq!(100 % attesters_per_slot, 0);
    let percent_per_attester = 100 / attesters_per_slot;
    assert_eq!(percent_parent_votes % percent_per_attester, 0);
    assert_eq!(percent_empty_votes % percent_per_attester, 0);
    assert_eq!(percent_head_votes % percent_per_attester, 0);
    let num_parent_votes = Some(attesters_per_slot * percent_parent_votes / 100);
    let num_empty_votes = Some(attesters_per_slot * percent_empty_votes / 100);
    let num_head_votes = Some(attesters_per_slot * percent_head_votes / 100);

    let tester = InteractiveTester::<E>::new_with_initializer_and_mutator(
        Some(spec),
        validator_count,
        None,
        Some(Box::new(move |builder| {
            builder
                .proposer_re_org_head_threshold(Some(ReOrgThreshold(re_org_threshold)))
                .proposer_re_org_max_epochs_since_finalization(Epoch::new(
                    max_epochs_since_finalization,
                ))
                .proposer_re_org_disallowed_offsets(
                    DisallowedReOrgOffsets::new::<E>(disallowed_offsets).unwrap(),
                )
        })),
    )
    .await;
    let harness = &tester.harness;
    let mock_el = harness.mock_execution_layer.as_ref().unwrap();
    let execution_ctx = mock_el.server.ctx.clone();
    let slot_clock = &harness.chain.slot_clock;

    // Move to terminal block.
    mock_el.server.all_payloads_valid();
    execution_ctx
        .execution_block_generator
        .write()
        .move_to_terminal_block()
        .unwrap();

    // Send proposer preparation data for all validators.
    let proposer_preparation_data = all_validators
        .iter()
        .map(|i| ProposerPreparationData {
            validator_index: *i as u64,
            fee_recipient: Address::from_low_u64_be(*i as u64),
        })
        .collect::<Vec<_>>();
    harness
        .chain
        .execution_layer
        .as_ref()
        .unwrap()
        .update_proposer_preparation(
            head_slot.epoch(E::slots_per_epoch()) + 1,
            &proposer_preparation_data,
        )
        .await;

    // Create some chain depth. Sign sync committee signatures so validator balances don't dip
    // below 32 ETH and become ineligible for withdrawals.
    harness.advance_slot();
    harness
        .extend_chain_with_sync(
            num_initial as usize,
            BlockStrategy::OnCanonicalHead,
            AttestationStrategy::AllValidators,
            SyncCommitteeStrategy::AllValidators,
        )
        .await;

    // Start collecting fork choice updates.
    let forkchoice_updates = Arc::new(Mutex::new(ForkChoiceUpdates::default()));
    let forkchoice_updates_inner = forkchoice_updates.clone();
    let chain_inner = harness.chain.clone();

    execution_ctx
        .hook
        .lock()
        .set_forkchoice_updated_hook(Box::new(move |state, payload_attributes| {
            let received_at = chain_inner.slot_clock.now_duration().unwrap();
            let state = ForkchoiceState::from(state);
            let payload_attributes = payload_attributes.map(Into::into);
            let update = ForkChoiceUpdateMetadata {
                received_at,
                state,
                payload_attributes,
            };
            forkchoice_updates_inner.lock().insert(update);
            None
        }));

    // We set up the following block graph, where B is a block that arrives late and is re-orged
    // by C.
    //
    // A | B | - |
    // ^ | - | C |

    let slot_a = Slot::new(num_initial + 1);
    let slot_b = slot_a + parent_distance;
    let slot_c = slot_b + head_distance;

    // We need to transition to at least epoch 2 in order to trigger
    // `process_rewards_and_penalties`. This allows us to test withdrawals changes at epoch
    // boundaries.
    if expect_withdrawals_change_on_epoch {
        assert!(
            slot_c.epoch(E::slots_per_epoch()) >= 2,
            "for withdrawals to change, test must end at an epoch >= 2"
        );
    }

    harness.advance_slot();
    let (block_a_root, block_a, state_a) = harness
        .add_block_at_slot(slot_a, harness.get_current_state())
        .await
        .unwrap();

    // Attest to block A during slot A.
    let (block_a_parent_votes, _) = harness.make_attestations_with_limit(
        &all_validators,
        &state_a,
        state_a.canonical_root(),
        block_a_root,
        slot_a,
        num_parent_votes,
    );
    harness.process_attestations(block_a_parent_votes);

    // Attest to block A during slot B.
    for _ in 0..parent_distance {
        harness.advance_slot();
    }
    let (block_a_empty_votes, block_a_attesters) = harness.make_attestations_with_limit(
        &all_validators,
        &state_a,
        state_a.canonical_root(),
        block_a_root,
        slot_b,
        num_empty_votes,
    );
    harness.process_attestations(block_a_empty_votes);

    let remaining_attesters = all_validators
        .iter()
        .copied()
        .filter(|index| !block_a_attesters.contains(index))
        .collect::<Vec<_>>();

    // Produce block B and process it halfway through the slot.
    let (block_b, mut state_b) = harness.make_block(state_a.clone(), slot_b).await;
    let block_b_root = block_b.0.canonical_root();

    let obs_time = slot_clock.start_of(slot_b).unwrap() + slot_clock.slot_duration() / 2;
    slot_clock.set_current_time(obs_time);
    harness.chain.block_times_cache.write().set_time_observed(
        block_b_root,
        slot_b,
        obs_time,
        None,
        None,
    );
    harness.process_block_result(block_b.clone()).await.unwrap();

    // Add attestations to block B.
    let (block_b_head_votes, _) = harness.make_attestations_with_limit(
        &remaining_attesters,
        &state_b,
        state_b.canonical_root(),
        block_b_root.into(),
        slot_b,
        num_head_votes,
    );
    harness.process_attestations(block_b_head_votes);

    let payload_lookahead = harness.chain.config.prepare_payload_lookahead;
    let fork_choice_lookahead = Duration::from_millis(500);
    while harness.get_current_slot() != slot_c {
        let current_slot = harness.get_current_slot();
        let next_slot = current_slot + 1;

        // Simulate the scheduled call to prepare proposers at 8 seconds into the slot.
        harness.advance_to_slot_lookahead(next_slot, payload_lookahead);
        harness
            .chain
            .prepare_beacon_proposer(current_slot)
            .await
            .unwrap();

        // Simulate the scheduled call to fork choice + prepare proposers 500ms before the
        // next slot.
        harness.advance_to_slot_lookahead(next_slot, fork_choice_lookahead);
        harness.chain.recompute_head_at_slot(next_slot).await;
        harness
            .chain
            .prepare_beacon_proposer(current_slot)
            .await
            .unwrap();

        harness.advance_slot();
        harness.chain.per_slot_task().await;
    }

    // Produce block C.
    // Advance state_b so we can get the proposer.
    assert_eq!(state_b.slot(), slot_b);
    let pre_advance_withdrawals = get_expected_withdrawals(&state_b, &harness.chain.spec)
        .unwrap()
        .0
        .to_vec();
    complete_state_advance(&mut state_b, None, slot_c, &harness.chain.spec).unwrap();

    let proposer_index = state_b
        .get_beacon_proposer_index(slot_c, &harness.chain.spec)
        .unwrap();
    let randao_reveal = harness
        .sign_randao_reveal(&state_b, proposer_index, slot_c)
        .into();
    let (unsigned_block_type, _) = tester
        .client
        .get_validator_blocks_v3::<E>(slot_c, &randao_reveal, None, None)
        .await
        .unwrap();

    let (unsigned_block_c, block_c_blobs) = match unsigned_block_type.data {
        ProduceBlockV3Response::Full(unsigned_block_contents_c) => {
            unsigned_block_contents_c.deconstruct()
        }
        ProduceBlockV3Response::Blinded(_) => {
            panic!("Should not be a blinded block");
        }
    };
    let block_c = Arc::new(harness.sign_beacon_block(unsigned_block_c, &state_b));

    if should_re_org {
        // Block C should build on A.
        assert_eq!(block_c.parent_root(), block_a_root.into());
    } else {
        // Block C should build on B.
        assert_eq!(block_c.parent_root(), block_b_root);
    }

    // Applying block C should cause it to become head regardless (re-org or continuation).
    let block_root_c = harness
        .process_block_result((block_c.clone(), block_c_blobs))
        .await
        .unwrap()
        .into();
    assert_eq!(harness.head_block_root(), block_root_c);

    // Check the fork choice updates that were sent.
    let forkchoice_updates = forkchoice_updates.lock();
    let block_a_exec_hash = block_a
        .0
        .message()
        .execution_payload()
        .unwrap()
        .block_hash();
    let block_b_exec_hash = block_b
        .0
        .message()
        .execution_payload()
        .unwrap()
        .block_hash();

    let block_c_timestamp = block_c.message().execution_payload().unwrap().timestamp();

    // If we re-orged then no fork choice update for B should have been sent.
    assert_eq!(
        should_re_org,
        !forkchoice_updates.contains_update_for(block_b_exec_hash),
        "{block_b_exec_hash:?}"
    );

    // Check the timing of the first fork choice update with payload attributes for block C.
    let c_parent_hash = if should_re_org {
        block_a_exec_hash
    } else {
        block_b_exec_hash
    };
    let first_update = forkchoice_updates
        .first_update_with_payload_attributes(c_parent_hash, block_c_timestamp)
        .unwrap();
    let payload_attribs = first_update.payload_attributes.as_ref().unwrap();

    // Check that withdrawals from the payload attributes match those computed from the parent's
    // advanced state.
    let expected_withdrawals = if should_re_org {
        let mut state_a_advanced = state_a.clone();
        complete_state_advance(&mut state_a_advanced, None, slot_c, &harness.chain.spec).unwrap();
        get_expected_withdrawals(&state_a_advanced, &harness.chain.spec)
    } else {
        get_expected_withdrawals(&state_b, &harness.chain.spec)
    }
    .unwrap()
    .0
    .to_vec();
    let payload_attribs_withdrawals = payload_attribs.withdrawals().unwrap();
    assert_eq!(expected_withdrawals, *payload_attribs_withdrawals);
    assert!(!expected_withdrawals.is_empty());

    if should_re_org
        || expect_withdrawals_change_on_epoch
            && slot_c.epoch(E::slots_per_epoch()) != slot_b.epoch(E::slots_per_epoch())
    {
        assert_ne!(expected_withdrawals, pre_advance_withdrawals);
    }

    // Check that the `parent_beacon_block_root` of the payload attributes are correct.
    if let Ok(parent_beacon_block_root) = payload_attribs.parent_beacon_block_root() {
        assert_eq!(parent_beacon_block_root, block_c.parent_root());
    }

    let lookahead = slot_clock
        .start_of(slot_c)
        .unwrap()
        .checked_sub(first_update.received_at)
        .unwrap();

    if !misprediction {
        assert_eq!(
            lookahead,
            payload_lookahead,
            "lookahead={lookahead:?}, timestamp={}, prev_randao={:?}",
            payload_attribs.timestamp(),
            payload_attribs.prev_randao(),
        );
    } else {
        // On a misprediction we issue the first fcU 500ms before creating a block!
        assert_eq!(
            lookahead,
            fork_choice_lookahead,
            "timestamp={}, prev_randao={:?}",
            payload_attribs.timestamp(),
            payload_attribs.prev_randao(),
        );
    }
}

// Test that running fork choice before proposing results in selection of the correct head.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
pub async fn fork_choice_before_proposal() {
    // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing
    // `validator_count // 32`.
    let validator_count = 64;
    let all_validators = (0..validator_count).collect::<Vec<_>>();
    let num_initial: u64 = 31;

    let tester = InteractiveTester::<E>::new(None, validator_count).await;
    let harness = &tester.harness;

    // Create some chain depth.
    harness.advance_slot();
    harness
        .extend_chain(
            num_initial as usize,
            BlockStrategy::OnCanonicalHead,
            AttestationStrategy::AllValidators,
        )
        .await;

    // We set up the following block graph, where B is a block that is temporarily orphaned by C,
    // but is then reinstated and built upon by D.
    //
    // A | B | - | D |
    // ^ | - | C |
    let slot_a = Slot::new(num_initial);
    let slot_b = slot_a + 1;
    let slot_c = slot_a + 2;
    let slot_d = slot_a + 3;

    let state_a = harness.get_current_state();
    let (block_b, state_b) = harness.make_block(state_a.clone(), slot_b).await;
    let block_root_b = harness
        .process_block(slot_b, block_b.0.canonical_root(), block_b)
        .await
        .unwrap();

    // Create attestations to B but keep them in reserve until after C has been processed.
    let attestations_b = harness.make_attestations(
        &all_validators,
        &state_b,
        state_b.tree_hash_root(),
        block_root_b,
        slot_b,
    );

    let (block_c, state_c) = harness.make_block(state_a, slot_c).await;
    let block_root_c = harness
        .process_block(slot_c, block_c.0.canonical_root(), block_c.clone())
        .await
        .unwrap();

    // Create attestations to C from a small number of validators and process them immediately.
    let attestations_c = harness.make_attestations(
        &all_validators[..validator_count / 2],
        &state_c,
        state_c.tree_hash_root(),
        block_root_c,
        slot_c,
    );
    harness.process_attestations(attestations_c);

    // Apply the attestations to B, but don't re-run fork choice.
    harness.process_attestations(attestations_b);

    // Due to proposer boost, the head should be C during slot C.
    assert_eq!(
        harness.chain.canonical_head.cached_head().head_block_root(),
        block_root_c.into()
    );

    // Ensure that building a block via the HTTP API re-runs fork choice and builds block D upon B.
    // Manually prod the per-slot task, because the slot timer doesn't run in the background in
    // these tests.
    harness.advance_slot();
    harness.chain.per_slot_task().await;

    let proposer_index = state_b
        .get_beacon_proposer_index(slot_d, &harness.chain.spec)
        .unwrap();
    let randao_reveal = harness
        .sign_randao_reveal(&state_b, proposer_index, slot_d)
        .into();
    let block_d = tester
        .client
        .get_validator_blocks::<E>(slot_d, &randao_reveal, None)
        .await
        .unwrap()
        .data
        .deconstruct()
        .0;

    // Head is now B.
    assert_eq!(
        harness.chain.canonical_head.cached_head().head_block_root(),
        block_root_b.into()
    );
    // D's parent is B.
    assert_eq!(block_d.parent_root(), block_root_b.into());
}

// Test that attestations to unknown blocks are requeued and processed when their block arrives.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn queue_attestations_from_http() {
    let validator_count = 128;
    let all_validators = (0..validator_count).collect::<Vec<_>>();

    let tester = InteractiveTester::<E>::new(None, validator_count).await;
    let harness = &tester.harness;
    let client = tester.client.clone();

    let num_initial = 5;

    // Slot of the block attested to.
    let attestation_slot = Slot::new(num_initial) + 1;

    // Make some initial blocks.
    harness.advance_slot();
    harness
        .extend_chain(
            num_initial as usize,
            BlockStrategy::OnCanonicalHead,
            AttestationStrategy::AllValidators,
        )
        .await;

    harness.advance_slot();
    assert_eq!(harness.get_current_slot(), attestation_slot);

    // Make the attested-to block without applying it.
    let pre_state = harness.get_current_state();
    let (block, post_state) = harness.make_block(pre_state, attestation_slot).await;
    let block_root = block.0.canonical_root();

    // Make attestations to the block and POST them to the beacon node on a background thread.
    let attestations = harness
        .make_unaggregated_attestations(
            &all_validators,
            &post_state,
            block.0.state_root(),
            block_root.into(),
            attestation_slot,
        )
        .into_iter()
        .flat_map(|attestations| attestations.into_iter().map(|(att, _subnet)| att))
        .collect::<Vec<_>>();

    let attestation_future = tokio::spawn(async move {
        client
            .post_beacon_pool_attestations(&attestations)
            .await
            .expect("attestations should be processed successfully")
    });

    // In parallel, apply the block. We need to manually notify the reprocess queue, because the
    // `beacon_chain` does not know about the queue and will not update it for us.
    let parent_root = block.0.parent_root();
    harness
        .process_block(attestation_slot, block_root, block)
        .await
        .unwrap();
    tester
        .ctx
        .beacon_processor_reprocess_send
        .as_ref()
        .unwrap()
        .send(ReprocessQueueMessage::BlockImported {
            block_root,
            parent_root,
        })
        .await
        .unwrap();

    attestation_future.await.unwrap();
}
