import pytest
from hypothesis import (
    given,
    settings,
    strategies as st,
)

from eth_utils import (
    ValidationError,
)

from eth.constants import (
    ZERO_HASH32,
)
from eth2.beacon.helpers import (
    get_crosslink_committees_at_slot,
)
from eth2.beacon.state_machines.forks.serenity.block_validation import (
    validate_attestation_aggregate_signature,
    validate_attestation_latest_crosslink_root,
    validate_attestation_justified_block_root,
    validate_attestation_justified_slot,
    validate_attestation_shard_block_root,
    validate_attestation_slot,
)
from eth2.beacon.tools.builder.validator import (
    create_mock_signed_attestation,
)
from eth2.beacon.types.attestation_data import AttestationData


@pytest.mark.parametrize(
    (
        'attestation_slot,'
        'current_slot,'
        'epoch_length,'
        'min_attestation_inclusion_delay,'
        'is_valid,'
    ),
    [
        (0, 5, 5, 1, True),
        (0, 5, 5, 5, True),
        (0, 5, 5, 6, False),  # attestation_slot + in_attestation_inclusion_delay > current_slot
        (7, 5, 10, 1, False),  # attestation_slot > current_slot
        (10, 20, 10, 2, True),
        (9, 20, 10, 2, False),  # attestation_slot + EPOCH_LENGTH < current_slot
    ]
)
def test_validate_attestation_slot(sample_attestation_data_params,
                                   attestation_slot,
                                   current_slot,
                                   epoch_length,
                                   min_attestation_inclusion_delay,
                                   is_valid):
    attestation_data = AttestationData(**sample_attestation_data_params).copy(
        slot=attestation_slot,
    )

    if is_valid:
        validate_attestation_slot(
            attestation_data,
            current_slot,
            epoch_length,
            min_attestation_inclusion_delay,
        )
    else:
        with pytest.raises(ValidationError):
            validate_attestation_slot(
                attestation_data,
                current_slot,
                epoch_length,
                min_attestation_inclusion_delay,
            )


@pytest.mark.parametrize(
    (
        'attestation_slot,'
        'attestation_justified_slot,'
        'current_slot,'
        'previous_justified_slot,'
        'justified_slot,'
        'epoch_length,'
        'is_valid,'
    ),
    [
        (13, 5, 14, 0, 5, 5, True),
        (13, 0, 14, 0, 5, 5, False),  # targeting previous_justified_slot, should be targeting justified_slot # noqa: E501
        (13, 20, 14, 0, 5, 5, False),  # targeting future slot, should be targeting justified_slot
        (29, 10, 30, 10, 20, 10, True),
        (29, 20, 30, 10, 20, 10, False),  # targeting justified_slot, should be targeting previous_justified_slot # noqa: E501
        (29, 36, 30, 10, 20, 10, False),  # targeting future slot,  should be targeting previous_justified_slot # noqa: E501
        (10, 10, 10, 10, 10, 10, True),
    ]
)
def test_validate_attestation_justified_slot(sample_attestation_data_params,
                                             attestation_slot,
                                             attestation_justified_slot,
                                             current_slot,
                                             previous_justified_slot,
                                             justified_slot,
                                             epoch_length,
                                             is_valid):
    attestation_data = AttestationData(**sample_attestation_data_params).copy(
        slot=attestation_slot,
        justified_slot=attestation_justified_slot,
    )

    if is_valid:
        validate_attestation_justified_slot(
            attestation_data,
            current_slot,
            previous_justified_slot,
            justified_slot,
            epoch_length,
        )
    else:
        with pytest.raises(ValidationError):
            validate_attestation_justified_slot(
                attestation_data,
                current_slot,
                previous_justified_slot,
                justified_slot,
                epoch_length,
            )


@pytest.mark.parametrize(
    (
        'attestation_justified_block_root,'
        'justified_block_root,'
        'is_valid,'
    ),
    [
        (b'\x42' * 32, b'\x35' * 32, False),  # attestation.justified_block_root != justified_block_root # noqa: E501
        (b'\x42' * 32, b'\x42' * 32, True),
    ]
)
def test_validate_attestation_justified_block_root(sample_attestation_data_params,
                                                   attestation_justified_block_root,
                                                   justified_block_root,
                                                   is_valid):
    attestation_data = AttestationData(**sample_attestation_data_params).copy(
        justified_block_root=attestation_justified_block_root,
    )

    if is_valid:
        validate_attestation_justified_block_root(
            attestation_data,
            justified_block_root
        )
    else:
        with pytest.raises(ValidationError):
            validate_attestation_justified_block_root(
                attestation_data,
                justified_block_root
            )


@pytest.mark.parametrize(
    (
        'attestation_latest_crosslink_root,'
        'attestation_shard_block_root,'
        'latest_crosslink_root,'
        'is_valid,'
    ),
    [
        (b'\x66' * 32, b'\x42' * 32, b'\x35' * 32, False),
        (b'\x42' * 32, b'\x42' * 32, b'\x66' * 32, False),
        (b'\x66' * 32, b'\x42' * 32, b'\x42' * 32, True),
        (b'\x42' * 32, b'\x35' * 32, b'\x42' * 32, True),
        (b'\x42' * 32, b'\x42' * 32, b'\x42' * 32, True),
    ]
)
def test_validate_attestation_latest_crosslink_root(sample_attestation_data_params,
                                                    attestation_latest_crosslink_root,
                                                    attestation_shard_block_root,
                                                    latest_crosslink_root,
                                                    is_valid):
    sample_attestation_data_params['latest_crosslink_root'] = attestation_latest_crosslink_root
    sample_attestation_data_params['shard_block_root'] = attestation_shard_block_root
    attestation_data = AttestationData(**sample_attestation_data_params).copy(
        latest_crosslink_root=attestation_latest_crosslink_root,
        shard_block_root=attestation_shard_block_root,
    )

    if is_valid:
        validate_attestation_latest_crosslink_root(
            attestation_data,
            latest_crosslink_root,
        )
    else:
        with pytest.raises(ValidationError):
            validate_attestation_latest_crosslink_root(
                attestation_data,
                latest_crosslink_root,
            )


@pytest.mark.parametrize(
    (
        'attestation_shard_block_root,'
        'is_valid,'
    ),
    [
        (ZERO_HASH32, True),
        (b'\x35' * 32, False),
        (b'\x66' * 32, False),
    ]
)
def test_validate_attestation_shard_block_root(sample_attestation_data_params,
                                               attestation_shard_block_root,
                                               is_valid):
    attestation_data = AttestationData(**sample_attestation_data_params).copy(
        shard_block_root=attestation_shard_block_root,
    )

    if is_valid:
        validate_attestation_shard_block_root(
            attestation_data,
        )
    else:
        with pytest.raises(ValidationError):
            validate_attestation_shard_block_root(
                attestation_data,
            )


@settings(max_examples=1)
@given(random=st.randoms())
@pytest.mark.parametrize(
    (
        'num_validators,'
        'epoch_length,'
        'target_committee_size,'
        'shard_count,'
        'is_valid,'
    ),
    [
        (10, 2, 2, 2, True),
        (40, 4, 3, 5, True),
        (20, 5, 3, 2, True),
        (20, 5, 3, 2, False),
    ],
)
def test_validate_attestation_aggregate_signature(genesis_state,
                                                  epoch_length,
                                                  random,
                                                  sample_attestation_data_params,
                                                  is_valid,
                                                  target_committee_size,
                                                  shard_count,
                                                  keymap):
    state = genesis_state

    # choose committee
    slot = 0
    crosslink_committee = get_crosslink_committees_at_slot(
        state=state,
        slot=slot,
        epoch_length=epoch_length,
        target_committee_size=target_committee_size,
        shard_count=shard_count,
    )[0]
    committee, shard = crosslink_committee
    committee_size = len(committee)
    assert committee_size > 0

    # randomly select 3/4 participants from committee
    votes_count = len(committee) * 3 // 4
    assert votes_count > 0

    attestation_data = AttestationData(**sample_attestation_data_params).copy(
        slot=slot,
        shard=shard,
    )

    attestation = create_mock_signed_attestation(
        state,
        attestation_data,
        committee,
        votes_count,
        keymap,
    )

    if is_valid:
        validate_attestation_aggregate_signature(
            state,
            attestation,
            epoch_length,
            target_committee_size,
            shard_count,
        )
    else:
        # mess up signature
        attestation = attestation.copy(
            aggregate_signature=(
                attestation.aggregate_signature[0] + 10,
                attestation.aggregate_signature[1] - 1
            )
        )
        with pytest.raises(ValidationError):
            validate_attestation_aggregate_signature(
                state,
                attestation,
                epoch_length,
                target_committee_size,
                shard_count,
            )
