
import time
from unittest.mock import PropertyMock

import pytest
from hexbytes import HexBytes
from web3.exceptions import TimeExhausted, TransactionNotFound

from tests.mock.interfaces import MockEthereumClient


@pytest.fixture()
def receipt():
    block_number_of_my_tx = 42
    my_tx_hash = HexBytes('0xFabadaAcabada')
    receipt = {
        'transactionHash': my_tx_hash,
        'blockNumber': block_number_of_my_tx,
        'blockHash': HexBytes('0xBebeCafe')
    }
    return receipt


def test_check_transaction_is_on_chain(mocker, mock_ethereum_client, receipt):
    # Mocking Web3 and EthereumClient
    web3_mock = mock_ethereum_client.w3
    web3_mock.eth.get_transaction_receipt = mocker.Mock(return_value=receipt)

    # Test with no chain reorganizations:

    # While web3 keeps returning the same receipt that we initially had, all good
    assert mock_ethereum_client.check_transaction_is_on_chain(receipt=receipt)

    # Test with chain re-organizations:

    # Let's assume that our TX ends up mined in a different block, and we receive a new receipt
    new_receipt = dict(receipt)
    new_receipt.update({'blockHash': HexBytes('0xBebeCebada')})

    web3_mock.eth.get_transaction_receipt = mocker.Mock(return_value=new_receipt)

    exception = mock_ethereum_client.ChainReorganizationDetected
    message = exception(receipt=receipt).message
    with pytest.raises(exception, match=message):
        _ = mock_ethereum_client.check_transaction_is_on_chain(receipt=receipt)

    # Another example: there has been a chain reorganization and our beloved TX is gone for good:
    web3_mock.eth.get_transaction_receipt = mocker.Mock(side_effect=TransactionNotFound)
    with pytest.raises(exception, match=message):
        _ = mock_ethereum_client.check_transaction_is_on_chain(receipt=receipt)


@pytest.mark.skip("Needs fix for web3.py v6+")
def test_block_until_enough_confirmations(mocker, mock_ethereum_client, receipt):
    my_tx_hash = receipt['transactionHash']
    block_number_of_my_tx = receipt['blockNumber']

    # Test that web3's TimeExhausted is propagated:
    web3_mock = mock_ethereum_client.w3
    web3_mock.eth.wait_for_transaction_receipt = mocker.Mock(side_effect=TimeExhausted)

    with pytest.raises(TimeExhausted):
        mock_ethereum_client.block_until_enough_confirmations(transaction_hash=my_tx_hash,
                                                              timeout=1,
                                                              confirmations=1)

    # Test that NotEnoughConfirmations is raised when there are not enough confirmations.
    # In this case, we're going to mock eth.blockNumber to be stuck
    web3_mock.eth.wait_for_transaction_receipt = mocker.Mock(return_value=receipt)
    web3_mock.eth.get_transaction_receipt = mocker.Mock(return_value=receipt)

    type(web3_mock.eth).block_number = PropertyMock(return_value=block_number_of_my_tx)  # See docs of PropertyMock

    # Additional adjustments to make the test faster
    mocker.patch.object(mock_ethereum_client, '_calculate_confirmations_timeout', return_value=0.1)
    mock_ethereum_client.BLOCK_CONFIRMATIONS_POLLING_TIME = 0

    with pytest.raises(mock_ethereum_client.NotEnoughConfirmations):
        mock_ethereum_client.block_until_enough_confirmations(transaction_hash=my_tx_hash,
                                                              timeout=1,
                                                              confirmations=1)

    # Test that block_until_enough_confirmations keeps iterating until the required confirmations are obtained
    required_confirmations = 3
    new_blocks_sequence = range(block_number_of_my_tx, block_number_of_my_tx + required_confirmations + 1)
    type(web3_mock.eth).blockNumber = PropertyMock(side_effect=new_blocks_sequence)  # See docs of PropertyMock
    spy_check_transaction = mocker.spy(mock_ethereum_client, 'check_transaction_is_on_chain')

    returned_receipt = mock_ethereum_client.block_until_enough_confirmations(transaction_hash=my_tx_hash,
                                                                             timeout=1,
                                                                             confirmations=required_confirmations)
    assert receipt == returned_receipt
    assert required_confirmations + 1 == spy_check_transaction.call_count


def test_wait_for_receipt_no_confirmations(mocker, mock_ethereum_client, receipt):
    my_tx_hash = receipt['transactionHash']

    # Test that web3's TimeExhausted is propagated:
    web3_mock = mock_ethereum_client.w3
    web3_mock.eth.wait_for_transaction_receipt = mocker.Mock(side_effect=TimeExhausted)
    with pytest.raises(TimeExhausted):
        _ = mock_ethereum_client.wait_for_receipt(transaction_hash=my_tx_hash, timeout=1, confirmations=0)
    web3_mock.eth.wait_for_transaction_receipt.assert_called_once_with(transaction_hash=my_tx_hash,
                                                                       timeout=1,
                                                                       poll_latency=MockEthereumClient.TRANSACTION_POLLING_TIME)

    # Test that when web3's layer returns the receipt, we get that receipt
    web3_mock.eth.wait_for_transaction_receipt = mocker.Mock(return_value=receipt)
    returned_receipt = mock_ethereum_client.wait_for_receipt(transaction_hash=my_tx_hash, timeout=1, confirmations=0)
    assert receipt == returned_receipt
    web3_mock.eth.wait_for_transaction_receipt.assert_called_once_with(transaction_hash=my_tx_hash,
                                                                       timeout=1,
                                                                       poll_latency=MockEthereumClient.TRANSACTION_POLLING_TIME)


def test_wait_for_receipt_with_confirmations(mocker, mock_ethereum_client, receipt):
    my_tx_hash = receipt['transactionHash']

    mock_ethereum_client.COOLING_TIME = 0  # Don't make test unnecessarily slow

    time_spy = mocker.spy(time, 'sleep')
    # timeout_spy = mocker.spy(Timeout, 'check')  # FIXME

    # First, let's make a simple, successful call to check that:
    #   - The same receipt goes through
    #   - The cooling time is respected
    mock_ethereum_client.block_until_enough_confirmations = mocker.Mock(return_value=receipt)
    returned_receipt = mock_ethereum_client.wait_for_receipt(transaction_hash=my_tx_hash, timeout=1, confirmations=1)
    assert receipt == returned_receipt
    time_spy.assert_called_once_with(mock_ethereum_client.COOLING_TIME)
    # timeout_spy.assert_not_called()  # FIXME

    # Test that wait_for_receipt finishes when a receipt is returned by block_until_enough_confirmations
    sequence_of_events = (
        TimeExhausted,
        mock_ethereum_client.ChainReorganizationDetected(receipt),
        mock_ethereum_client.NotEnoughConfirmations,
        receipt
    )
    timeout = None

    mock_ethereum_client.BLOCK_CONFIRMATIONS_POLLING_TIME = 0
    mock_ethereum_client.block_until_enough_confirmations = mocker.Mock(side_effect=sequence_of_events)

    returned_receipt = mock_ethereum_client.wait_for_receipt(transaction_hash=my_tx_hash, timeout=timeout, confirmations=1)
    assert receipt == returned_receipt
    # assert timeout_spy.call_count == 3  # FIXME

    # Test that a TransactionTimeout is thrown when no receipt is found during the given time
    timeout = 0.1
    sequence_of_events = [TimeExhausted] * 10
    mock_ethereum_client.BLOCK_CONFIRMATIONS_POLLING_TIME = 0.015
    mock_ethereum_client.block_until_enough_confirmations = mocker.Mock(side_effect=sequence_of_events)
    with pytest.raises(mock_ethereum_client.TransactionTimeout):
        _ = mock_ethereum_client.wait_for_receipt(transaction_hash=my_tx_hash,
                                                  timeout=timeout,
                                                  confirmations=1)
