import copy

import pytest

from eth2.beacon.chains.base import BeaconChain
from eth2.beacon.chains.testnet import SkeletonLakeChain
from eth2.beacon.committee_helpers import get_beacon_proposer_index
from eth2.beacon.constants import GENESIS_SLOT
from eth2.beacon.db.exceptions import AttestationRootNotFound, StateNotFound
from eth2.beacon.state_machines.forks.serenity.blocks import SerenitySignedBeaconBlock
from eth2.beacon.tools.builder.proposer import create_block, generate_randao_reveal
from eth2.beacon.tools.builder.validator import create_mock_signed_attestations_at_slot
from eth2.beacon.types.blocks import SignedBeaconBlock
from eth2.beacon.typing import FromBlockParams


@pytest.fixture
def chain(beacon_chain_without_block_validation):
    return beacon_chain_without_block_validation


@pytest.fixture
def valid_chain(beacon_chain_with_block_validation):
    return beacon_chain_with_block_validation


@pytest.mark.parametrize(
    ("validator_count,slots_per_epoch,target_committee_size,max_committees_per_slot"),
    [(100, 20, 10, 20)],
)
def test_canonical_chain(valid_chain, fork_choice_scoring):
    genesis_block = valid_chain.get_canonical_block_by_slot(GENESIS_SLOT)

    # Our chain fixture is created with only the genesis header, so initially that's the head of
    # the canonical chain.
    assert valid_chain.get_canonical_head() == genesis_block

    block = SerenitySignedBeaconBlock.from_parent(
        genesis_block, FromBlockParams(slot=1)
    )
    valid_chain.chaindb.persist_block(block, block.__class__, fork_choice_scoring)

    assert valid_chain.get_canonical_head() == block
    state_machine = valid_chain.get_state_machine(block.slot)
    scoring = state_machine.get_fork_choice_scoring()

    assert scoring.score(block.message) != 0

    canonical_block_1 = valid_chain.get_canonical_block_by_slot(genesis_block.slot + 1)
    assert canonical_block_1 == block

    result_block = valid_chain.get_block_by_root(block.message.hash_tree_root)
    assert result_block == block


@pytest.mark.parametrize(
    (
        "validator_count,"
        "slots_per_epoch,"
        "target_committee_size,"
        "max_committees_per_slot,"
    ),
    [(100, 16, 10, 16)],
)
def test_get_state_by_slot(valid_chain, genesis_block, genesis_state, keymap):
    # First, skip block and check if `get_state_by_slot` returns the expected state
    state_machine = valid_chain.get_state_machine(genesis_block.slot)
    config = state_machine.config
    state = valid_chain.get_head_state()
    block_skipped_slot = genesis_block.slot + 1
    block_skipped_state, _ = state_machine.apply_state_transition(
        state, future_slot=block_skipped_slot
    )
    valid_chain.chaindb.persist_state(block_skipped_state)
    with pytest.raises(StateNotFound):
        valid_chain.get_state_by_slot(block_skipped_slot)

    # Next, import proposed block and check if `get_state_by_slot` returns the expected state
    proposed_slot = block_skipped_slot + 1
    future_state, _ = state_machine.apply_state_transition(
        block_skipped_state, future_slot=proposed_slot
    )
    proposer_index = get_beacon_proposer_index(future_state, config)
    proposer = block_skipped_state.validators[proposer_index].pubkey
    private_key = keymap[proposer]
    randao_reveal = generate_randao_reveal(
        private_key, proposed_slot, block_skipped_state, config
    )
    block = create_block(
        proposed_slot,
        genesis_block.message.hash_tree_root,
        randao_reveal,
        state.eth1_data,
        (),
        block_skipped_state,
        state_machine,
        private_key,
    )
    valid_chain.import_block(block)
    state = valid_chain.get_head_state()
    assert (
        valid_chain.get_state_by_slot(proposed_slot).hash_tree_root
        == state.hash_tree_root
    )


@pytest.mark.long
@pytest.mark.parametrize(
    ("validator_count,slots_per_epoch,target_committee_size,max_committees_per_slot"),
    [(100, 16, 10, 16)],
)
def test_import_blocks(valid_chain, genesis_block, genesis_state, config, keymap):
    state = genesis_state
    blocks = (genesis_block,)
    valid_chain_2 = copy.deepcopy(valid_chain)
    for _ in range(3):
        state_machine = valid_chain.get_state_machine(blocks[-1].slot)
        parent_block = blocks[-1]
        slot = state.slot + 2

        future_state, _ = state_machine.apply_state_transition(
            state, future_slot=state.slot + 2
        )
        proposer_index = get_beacon_proposer_index(future_state, config)
        proposer = state.validators[proposer_index].pubkey
        private_key = keymap[proposer]
        randao_reveal = generate_randao_reveal(private_key, slot, state, config)
        block = create_block(
            slot,
            parent_block.message.hash_tree_root,
            randao_reveal,
            state.eth1_data,
            (),
            state,
            state_machine,
            private_key,
        )

        valid_chain.import_block(block)
        assert valid_chain.get_canonical_head() == block

        state = valid_chain.get_head_state()

        assert block == valid_chain.get_canonical_block_by_slot(block.slot)
        assert block.message.hash_tree_root == valid_chain.get_canonical_block_root(
            block.slot
        )
        blocks += (block,)

    assert valid_chain.get_canonical_head() != valid_chain_2.get_canonical_head()

    for block in blocks[1:]:
        valid_chain_2.import_block(block)

    assert valid_chain.get_canonical_head() == valid_chain_2.get_canonical_head()
    assert valid_chain.get_head_state().slot != 0
    assert valid_chain.get_head_state() == valid_chain_2.get_head_state()


def test_from_genesis(base_db, genesis_block, genesis_state, fixture_sm_class, config):
    klass = BeaconChain.configure(
        __name__="TestChain", sm_configuration=((0, fixture_sm_class),)
    )

    assert type(genesis_block) == SerenitySignedBeaconBlock
    block = SignedBeaconBlock.create(message=genesis_block.message)
    assert type(block) == SignedBeaconBlock

    chain = klass.from_genesis(base_db, genesis_state)
    assert chain


@pytest.mark.long
@pytest.mark.parametrize(
    (
        "validator_count,"
        "slots_per_epoch,"
        "target_committee_size,"
        "max_committees_per_slot,"
        "min_attestation_inclusion_delay,"
    ),
    [(100, 16, 10, 16, 0)],
)
def test_get_attestation_root(
    valid_chain, genesis_block, genesis_state, keymap, min_attestation_inclusion_delay
):
    state_machine = valid_chain.get_state_machine()
    config = state_machine.config
    attestations = create_mock_signed_attestations_at_slot(
        state=genesis_state,
        config=config,
        state_machine=state_machine,
        attestation_slot=genesis_block.slot,
        beacon_block_root=genesis_block.message.hash_tree_root,
        keymap=keymap,
    )

    state = genesis_state
    slot = genesis_state.slot + 1
    future_state, _ = state_machine.apply_state_transition(state, future_slot=slot)
    proposer_index = get_beacon_proposer_index(future_state, config)
    proposer = state.validators[proposer_index].pubkey
    private_key = keymap[proposer]
    randao_reveal = generate_randao_reveal(private_key, slot, state, config)
    block = create_block(
        slot,
        genesis_block.message.hash_tree_root,
        randao_reveal,
        state.eth1_data,
        attestations,
        state,
        state_machine,
        private_key,
    )
    valid_chain.import_block(block)
    # Only one attestation in attestations, so just check that one
    a0 = attestations[0]
    assert valid_chain.get_attestation_by_root(a0.hash_tree_root) == a0
    assert valid_chain.attestation_exists(a0.hash_tree_root)
    fake_attestation = a0.set("signature", b"\x78" * 96)
    with pytest.raises(AttestationRootNotFound):
        valid_chain.get_attestation_by_root(fake_attestation.hash_tree_root)
    assert not valid_chain.attestation_exists(fake_attestation.hash_tree_root)


@pytest.mark.parametrize("chain_klass", (SkeletonLakeChain,))
def test_chain_class_well_defined(base_db, chain_klass):
    chain = chain_klass(base_db)
    assert chain.sm_configuration is not () and chain.sm_configuration is not None
