# pylint: disable=invalid-name,too-many-locals
import random

import pytest

from raiden.constants import EMPTY_HASH, EMPTY_HASH_KECCAK, UINT64_MAX
from raiden.settings import DEFAULT_NUMBER_OF_BLOCK_CONFIRMATIONS
from raiden.tests.utils import factories
from raiden.tests.utils.events import must_contain_entry
from raiden.tests.utils.factories import (
    HOP1,
    UNIT_SECRET,
    UNIT_SECRETHASH,
    UNIT_TRANSFER_PKEY,
    UNIT_TRANSFER_SENDER,
    UNIT_TRANSFER_TARGET,
)
from raiden.transfer import channel
from raiden.transfer.events import ContractSendSecretReveal, SendProcessed
from raiden.transfer.mediated_transfer import target
from raiden.transfer.mediated_transfer.events import (
    EventUnlockClaimFailed,
    SendSecretRequest,
    SendSecretReveal,
)
from raiden.transfer.mediated_transfer.state import TargetTransferState
from raiden.transfer.mediated_transfer.state_change import (
    ActionInitTarget,
    ReceiveLockExpired,
    ReceiveSecretReveal,
)
from raiden.transfer.state import EMPTY_MERKLE_ROOT
from raiden.transfer.state_change import Block, ContractReceiveSecretReveal, ReceiveUnlock


def make_target_state(
        our_address,
        amount,
        block_number,
        initiator,
        expiration=None,
        pseudo_random_generator=None,
):
    pseudo_random_generator = pseudo_random_generator or random.Random()

    from_channel = factories.make_channel(
        our_address=our_address,
        partner_address=UNIT_TRANSFER_SENDER,
        partner_balance=amount,
    )
    from_route = factories.route_from_channel(from_channel)

    if expiration is None:
        expiration = from_channel.reveal_timeout + block_number + 1

    from_transfer = factories.make_signed_transfer_for(
        from_channel,
        amount,
        initiator,
        our_address,
        expiration,
        UNIT_SECRET,
    )

    state_change = ActionInitTarget(from_route, from_transfer)
    iteration = target.handle_inittarget(
        state_change,
        from_channel,
        pseudo_random_generator,
        block_number,
    )
    return from_channel, iteration.new_state


def test_events_for_onchain_secretreveal():
    """ Secret must be registered on-chain when the unsafe region is reached and
    the secret is known.
    """
    amount = 3
    block_number = 10
    expiration = block_number + 30
    initiator = HOP1
    target_address = UNIT_TRANSFER_TARGET

    from_channel = factories.make_channel(
        our_address=target_address,
        partner_address=UNIT_TRANSFER_SENDER,
        partner_balance=amount,
    )
    from_route = factories.route_from_channel(from_channel)

    from_transfer = factories.make_signed_transfer_for(
        from_channel,
        amount,
        initiator,
        target_address,
        expiration,
        UNIT_SECRET,
    )

    channel.handle_receive_lockedtransfer(
        from_channel,
        from_transfer,
    )

    channel.register_offchain_secret(from_channel, UNIT_SECRET, UNIT_SECRETHASH)

    safe_to_wait = expiration - from_channel.reveal_timeout - 1
    unsafe_to_wait = expiration - from_channel.reveal_timeout

    state = TargetTransferState(from_route, from_transfer)
    events = target.events_for_onchain_secretreveal(state, from_channel, safe_to_wait)
    assert not events

    events = target.events_for_onchain_secretreveal(state, from_channel, unsafe_to_wait)
    assert events
    assert isinstance(events[0], ContractSendSecretReveal)
    assert events[0].secret == UNIT_SECRET


def test_handle_inittarget():
    """ Init transfer must send a secret request if the expiration is valid. """
    amount = 3
    block_number = 1
    initiator = factories.HOP1
    target_address = UNIT_TRANSFER_TARGET
    pseudo_random_generator = random.Random()

    from_channel = factories.make_channel(
        our_address=target_address,
        partner_address=UNIT_TRANSFER_SENDER,
        partner_balance=amount,
    )
    from_route = factories.route_from_channel(from_channel)

    expiration = from_channel.reveal_timeout + block_number + 1
    from_transfer = factories.make_signed_transfer(
        amount,
        initiator,
        target_address,
        expiration,
        UNIT_SECRET,
        channel_identifier=from_channel.identifier,
        token_network_address=from_channel.token_network_identifier,
    )

    state_change = ActionInitTarget(
        from_route,
        from_transfer,
    )

    iteration = target.handle_inittarget(
        state_change,
        from_channel,
        pseudo_random_generator,
        block_number,
    )

    assert must_contain_entry(iteration.events, SendSecretRequest, {
        'payment_identifier': from_transfer.payment_identifier,
        'amount': from_transfer.lock.amount,
        'secrethash': from_transfer.lock.secrethash,
        'recipient': initiator,
    })
    assert must_contain_entry(iteration.events, SendProcessed, {})


def test_handle_inittarget_bad_expiration():
    """ Init transfer must do nothing if the expiration is bad. """
    block_number = 1
    amount = 3
    initiator = factories.HOP1
    target_address = UNIT_TRANSFER_TARGET
    pseudo_random_generator = random.Random()

    from_channel = factories.make_channel(
        our_address=target_address,
        partner_address=UNIT_TRANSFER_SENDER,
        partner_balance=amount,
    )
    from_route = factories.route_from_channel(from_channel)

    expiration = from_channel.reveal_timeout + block_number + 1
    from_transfer = factories.make_signed_transfer_for(
        from_channel,
        amount,
        initiator,
        target_address,
        expiration,
        UNIT_SECRET,
    )

    channel.handle_receive_lockedtransfer(
        from_channel,
        from_transfer,
    )

    state_change = ActionInitTarget(from_route, from_transfer)
    iteration = target.handle_inittarget(
        state_change,
        from_channel,
        pseudo_random_generator,
        block_number,
    )
    assert must_contain_entry(iteration.events, EventUnlockClaimFailed, {})


def test_handle_offchain_secretreveal():
    """ The target node needs to inform the secret to the previous node to
    receive an updated balance proof.
    """
    amount = 3
    block_number = 1
    expiration = block_number + factories.UNIT_REVEAL_TIMEOUT
    initiator = factories.HOP1
    our_address = factories.ADDR
    secret = factories.UNIT_SECRET
    pseudo_random_generator = random.Random()

    channel_state, state = make_target_state(
        our_address,
        amount,
        block_number,
        initiator,
        expiration,
    )
    state_change = ReceiveSecretReveal(secret, initiator)
    iteration = target.handle_offchain_secretreveal(
        target_state=state,
        state_change=state_change,
        channel_state=channel_state,
        pseudo_random_generator=pseudo_random_generator,
        block_number=block_number,
    )
    assert len(iteration.events) == 1

    reveal = iteration.events[0]
    assert isinstance(reveal, SendSecretReveal)

    assert iteration.new_state.state == 'reveal_secret'
    assert reveal.secret == secret
    assert reveal.recipient == state.route.node_address

    # if we get an empty hash secret make sure it's rejected
    secret = EMPTY_HASH
    state_change = ReceiveSecretReveal(secret, initiator)
    iteration = target.handle_offchain_secretreveal(
        target_state=state,
        state_change=state_change,
        channel_state=channel_state,
        pseudo_random_generator=pseudo_random_generator,
        block_number=block_number,
    )
    assert len(iteration.events) == 0


def test_handle_offchain_secretreveal_after_lock_expired():
    """Test that getting the secret revealed after lock expiration for the
    target does not end up continuously emitting EventUnlockClaimFailed

    Target part for https://github.com/raiden-network/raiden/issues/3086
    """
    amount = 3
    block_number = 1
    expiration = block_number + factories.UNIT_REVEAL_TIMEOUT
    initiator = factories.HOP1
    our_address = factories.ADDR
    secret = factories.UNIT_SECRET
    pseudo_random_generator = random.Random()

    channel_state, state = make_target_state(
        our_address,
        amount,
        block_number,
        initiator,
        expiration,
    )

    lock_expiration = state.transfer.lock.expiration
    lock_expiration_block_number = lock_expiration + DEFAULT_NUMBER_OF_BLOCK_CONFIRMATIONS * 2
    lock_expiration_block = Block(
        block_number=lock_expiration_block_number,
        gas_limit=1,
        block_hash=factories.make_transaction_hash(),
    )
    iteration = target.state_transition(
        target_state=state,
        state_change=lock_expiration_block,
        channel_state=channel_state,
        pseudo_random_generator=pseudo_random_generator,
        block_number=lock_expiration_block_number,
    )
    state = iteration.new_state

    msg = 'At the expiration block we should get an EventUnlockClaimFailed'
    assert must_contain_entry(iteration.events, EventUnlockClaimFailed, {}), msg

    iteration = target.state_transition(
        target_state=state,
        state_change=ReceiveSecretReveal(secret, initiator),
        channel_state=channel_state,
        pseudo_random_generator=pseudo_random_generator,
        block_number=lock_expiration_block_number + 1,
    )
    state = iteration.new_state

    next_block = Block(
        block_number=lock_expiration_block_number + 1,
        gas_limit=1,
        block_hash=factories.make_transaction_hash(),
    )
    iteration = target.state_transition(
        target_state=state,
        state_change=next_block,
        channel_state=channel_state,
        pseudo_random_generator=pseudo_random_generator,
        block_number=lock_expiration_block_number + 1,
    )
    msg = 'At the next block we should not get the same event'
    assert not must_contain_entry(iteration.events, EventUnlockClaimFailed, {}), msg


def test_handle_onchain_secretreveal():
    """ The target node must update the lock state when the secret is
    registered in the blockchain.
    """
    amount = 3
    block_number = 1
    expiration = block_number + factories.UNIT_REVEAL_TIMEOUT
    initiator = factories.HOP1
    our_address = factories.ADDR
    secret = factories.UNIT_SECRET
    pseudo_random_generator = random.Random()

    channel_state, state = make_target_state(
        our_address,
        amount,
        block_number,
        initiator,
        expiration,
    )
    assert factories.UNIT_SECRETHASH in channel_state.partner_state.secrethashes_to_lockedlocks

    offchain_secret_reveal_iteration = target.state_transition(
        state,
        ReceiveSecretReveal(secret, initiator),
        channel_state,
        pseudo_random_generator,
        block_number,
    )
    assert factories.UNIT_SECRETHASH in channel_state.partner_state.secrethashes_to_unlockedlocks
    assert factories.UNIT_SECRETHASH not in channel_state.partner_state.secrethashes_to_lockedlocks

    # Make sure that an emptyhash on chain reveal is rejected.
    block_number_prior_the_expiration = expiration - 2
    onchain_reveal = ContractReceiveSecretReveal(
        transaction_hash=factories.make_address(),
        secret_registry_address=factories.make_address(),
        secrethash=EMPTY_HASH_KECCAK,
        secret=EMPTY_HASH,
        block_number=block_number_prior_the_expiration,
    )
    onchain_secret_reveal_iteration = target.state_transition(
        offchain_secret_reveal_iteration.new_state,
        onchain_reveal,
        channel_state,
        pseudo_random_generator,
        block_number_prior_the_expiration,
    )
    unlocked_onchain = channel_state.partner_state.secrethashes_to_onchain_unlockedlocks
    assert EMPTY_HASH_KECCAK not in unlocked_onchain

    # now let's go for the actual secret
    onchain_reveal.secret = secret
    onchain_reveal.secrethash = factories.UNIT_SECRETHASH
    onchain_secret_reveal_iteration = target.state_transition(
        offchain_secret_reveal_iteration.new_state,
        onchain_reveal,
        channel_state,
        pseudo_random_generator,
        block_number_prior_the_expiration,
    )
    unlocked_onchain = channel_state.partner_state.secrethashes_to_onchain_unlockedlocks
    assert factories.UNIT_SECRETHASH in unlocked_onchain

    # Check that after we register a lock on-chain handling the block again will
    # not cause us to attempt an onchain re-register
    extra_block_handle_transition = target.handle_block(
        onchain_secret_reveal_iteration.new_state,
        channel_state,
        block_number_prior_the_expiration + 1,
    )
    assert len(extra_block_handle_transition.events) == 0


def test_handle_block():
    """ Increase the block number. """
    initiator = factories.HOP6
    our_address = factories.ADDR
    amount = 3
    block_number = 1
    pseudo_random_generator = random.Random()

    from_channel, state = make_target_state(
        our_address,
        amount,
        block_number,
        initiator,
    )

    new_block = Block(
        block_number=block_number + 1,
        gas_limit=1,
        block_hash=factories.make_transaction_hash(),
    )
    iteration = target.state_transition(
        state,
        new_block,
        from_channel,
        pseudo_random_generator,
        new_block.block_number,
    )
    assert iteration.new_state
    assert not iteration.events


def test_handle_block_equal_block_number():
    """ Nothing changes. """
    initiator = factories.HOP6
    our_address = factories.ADDR
    amount = 3
    block_number = 1
    pseudo_random_generator = random.Random()

    from_channel, state = make_target_state(
        our_address,
        amount,
        block_number,
        initiator,
    )

    new_block = Block(
        block_number=block_number,
        gas_limit=1,
        block_hash=factories.make_transaction_hash(),
    )
    iteration = target.state_transition(
        state,
        new_block,
        from_channel,
        pseudo_random_generator,
        new_block.block_number,
    )
    assert iteration.new_state
    assert not iteration.events


def test_handle_block_lower_block_number():
    """ Nothing changes. """
    initiator = factories.HOP6
    our_address = factories.ADDR
    amount = 3
    block_number = 10
    pseudo_random_generator = random.Random()

    from_channel, state = make_target_state(
        our_address,
        amount,
        block_number,
        initiator,
    )

    new_block = Block(
        block_number=block_number - 1,
        gas_limit=1,
        block_hash=factories.make_transaction_hash(),
    )
    iteration = target.state_transition(
        state,
        new_block,
        from_channel,
        pseudo_random_generator,
        new_block.block_number,
    )
    assert iteration.new_state
    assert not iteration.events


def test_state_transition():
    """ Happy case testing. """
    lock_amount = 7
    block_number = 1
    initiator = factories.HOP6
    pseudo_random_generator = random.Random()

    our_balance = 100
    our_address = factories.make_address()
    partner_balance = 130

    from_channel = factories.make_channel(
        our_address=our_address,
        our_balance=our_balance,
        partner_address=UNIT_TRANSFER_SENDER,
        partner_balance=partner_balance,
    )
    from_route = factories.route_from_channel(from_channel)
    expiration = block_number + from_channel.settle_timeout - from_channel.reveal_timeout

    from_transfer = factories.make_signed_transfer_for(
        from_channel,
        lock_amount,
        initiator,
        our_address,
        expiration,
        UNIT_SECRET,
    )

    init = ActionInitTarget(
        from_route,
        from_transfer,
    )

    init_transition = target.state_transition(
        None,
        init,
        from_channel,
        pseudo_random_generator,
        block_number,
    )
    assert init_transition.new_state is not None
    assert init_transition.new_state.route == from_route
    assert init_transition.new_state.transfer == from_transfer

    first_new_block = Block(
        block_number=block_number + 1,
        gas_limit=1,
        block_hash=factories.make_transaction_hash(),
    )
    first_block_iteration = target.state_transition(
        init_transition.new_state,
        first_new_block,
        from_channel,
        pseudo_random_generator,
        first_new_block.block_number,
    )

    secret_reveal = ReceiveSecretReveal(factories.UNIT_SECRET, initiator)
    reveal_iteration = target.state_transition(
        first_block_iteration.new_state,
        secret_reveal,
        from_channel,
        pseudo_random_generator,
        first_new_block.block_number,
    )
    assert reveal_iteration.events

    second_new_block = Block(
        block_number=block_number + 2,
        gas_limit=1,
        block_hash=factories.make_transaction_hash(),
    )
    iteration = target.state_transition(
        init_transition.new_state,
        second_new_block,
        from_channel,
        pseudo_random_generator,
        second_new_block.block_number,
    )
    assert not iteration.events

    nonce = from_transfer.balance_proof.nonce + 1
    transferred_amount = lock_amount
    locksroot = EMPTY_MERKLE_ROOT
    invalid_message_hash = b'\x00' * 32
    locked_amount = 0

    balance_proof = factories.make_signed_balance_proof(
        nonce,
        transferred_amount,
        locked_amount,
        from_channel.token_network_identifier,
        from_route.channel_identifier,
        locksroot,
        invalid_message_hash,
        UNIT_TRANSFER_PKEY,
        UNIT_TRANSFER_SENDER,
    )

    balance_proof_state_change = ReceiveUnlock(
        message_identifier=random.randint(0, UINT64_MAX),
        secret=UNIT_SECRET,
        balance_proof=balance_proof,
    )

    proof_iteration = target.state_transition(
        init_transition.new_state,
        balance_proof_state_change,
        from_channel,
        pseudo_random_generator,
        block_number + 2,
    )
    assert proof_iteration.new_state is None


def test_target_reject_keccak_empty_hash():
    lock_amount = 7
    block_number = 1
    initiator = factories.HOP6
    pseudo_random_generator = random.Random()

    our_balance = 100
    our_address = factories.make_address()
    partner_balance = 130

    from_channel = factories.make_channel(
        our_address=our_address,
        our_balance=our_balance,
        partner_address=UNIT_TRANSFER_SENDER,
        partner_balance=partner_balance,
    )
    from_route = factories.route_from_channel(from_channel)
    expiration = block_number + from_channel.settle_timeout - from_channel.reveal_timeout

    from_transfer = factories.make_signed_transfer_for(
        channel_state=from_channel,
        amount=lock_amount,
        initiator=initiator,
        target=our_address,
        expiration=expiration,
        secret=EMPTY_HASH,
        allow_invalid=True,
    )

    init = ActionInitTarget(
        route=from_route,
        transfer=from_transfer,
    )

    init_transition = target.state_transition(
        None,
        init,
        from_channel,
        pseudo_random_generator,
        block_number,
    )
    assert init_transition.new_state is None


def test_target_receive_lock_expired():
    lock_amount = 7
    block_number = 1
    initiator = factories.HOP6
    pseudo_random_generator = random.Random()

    our_balance = 100
    our_address = factories.make_address()
    partner_balance = 130

    from_channel = factories.make_channel(
        our_address=our_address,
        our_balance=our_balance,
        partner_address=UNIT_TRANSFER_SENDER,
        partner_balance=partner_balance,
    )
    from_route = factories.route_from_channel(from_channel)
    expiration = block_number + from_channel.settle_timeout - from_channel.reveal_timeout

    from_transfer = factories.make_signed_transfer_for(
        channel_state=from_channel,
        amount=lock_amount,
        initiator=initiator,
        target=our_address,
        expiration=expiration,
        secret=UNIT_SECRET,
    )

    init = ActionInitTarget(
        from_route,
        from_transfer,
    )

    init_transition = target.state_transition(
        None,
        init,
        from_channel,
        pseudo_random_generator,
        block_number,
    )
    assert init_transition.new_state is not None
    assert init_transition.new_state.route == from_route
    assert init_transition.new_state.transfer == from_transfer

    balance_proof = factories.make_signed_balance_proof(
        2,
        from_transfer.balance_proof.transferred_amount,
        0,
        from_transfer.balance_proof.token_network_identifier,
        from_channel.identifier,
        EMPTY_MERKLE_ROOT,
        from_transfer.lock.secrethash,
        sender_address=UNIT_TRANSFER_SENDER,
        private_key=UNIT_TRANSFER_PKEY,
    )

    lock_expired_state_change = ReceiveLockExpired(
        balance_proof=balance_proof,
        secrethash=from_transfer.lock.secrethash,
        message_identifier=1,
    )

    block_before_confirmed_expiration = expiration + DEFAULT_NUMBER_OF_BLOCK_CONFIRMATIONS - 1
    iteration = target.state_transition(
        init_transition.new_state,
        lock_expired_state_change,
        from_channel,
        pseudo_random_generator,
        block_before_confirmed_expiration,
    )
    assert not must_contain_entry(iteration.events, SendProcessed, {})

    block_lock_expired = block_before_confirmed_expiration + 1
    iteration = target.state_transition(
        init_transition.new_state,
        lock_expired_state_change,
        from_channel,
        pseudo_random_generator,
        block_lock_expired,
    )
    assert must_contain_entry(iteration.events, SendProcessed, {})


def test_target_lock_is_expired_if_secret_is_not_registered_onchain():
    lock_amount = 7
    block_number = 1
    initiator = factories.HOP6
    pseudo_random_generator = random.Random()

    our_balance = 100
    our_address = factories.make_address()
    partner_balance = 130

    from_channel = factories.make_channel(
        our_address=our_address,
        our_balance=our_balance,
        partner_address=UNIT_TRANSFER_SENDER,
        partner_balance=partner_balance,
    )
    from_route = factories.route_from_channel(from_channel)
    expiration = block_number + from_channel.settle_timeout - from_channel.reveal_timeout

    from_transfer = factories.make_signed_transfer_for(
        from_channel,
        lock_amount,
        initiator,
        our_address,
        expiration,
        UNIT_SECRET,
    )

    init = ActionInitTarget(
        from_route,
        from_transfer,
    )

    init_transition = target.state_transition(
        None,
        init,
        from_channel,
        pseudo_random_generator,
        block_number,
    )
    assert init_transition.new_state is not None

    secret_reveal_iteration = target.state_transition(
        target_state=init_transition.new_state,
        state_change=ReceiveSecretReveal(UNIT_SECRET, from_channel.partner_state.address),
        channel_state=from_channel,
        pseudo_random_generator=pseudo_random_generator,
        block_number=block_number,
    )

    expired_block_number = from_transfer.lock.expiration + DEFAULT_NUMBER_OF_BLOCK_CONFIRMATIONS
    iteration = target.state_transition(
        target_state=secret_reveal_iteration.new_state,
        state_change=Block(expired_block_number, None, None),
        channel_state=from_channel,
        pseudo_random_generator=pseudo_random_generator,
        block_number=expired_block_number,
    )
    assert must_contain_entry(iteration.events, EventUnlockClaimFailed, {})


@pytest.mark.xfail(reason='Not implemented #522')
def test_transfer_successful_after_secret_learned():
    # TransferCompleted event must be used only after the secret is learned and
    # there is enough time to unlock the lock on chain.
    #
    # A mediated transfer might be received during the settlement period of the
    # current channel. The secret request is sent to the initiator and at the time
    # the secret is revealed there might not be enough time to safely unlock
    # the token on-chain.
    raise NotImplementedError()
