def test_mining():
    """Test the mining process in the Block"""

    block = Block(0, 'Test data', timestamp=0)

    block.difficulty = 8

    assert block.hash_satisfies_difficulty is False
    assert block.is_valid is False

    # Calculate a valid proof
    assert block.mining()

    assert block.hash_satisfies_difficulty
    assert block.is_valid

    # Set a value which cannot get a valid result
    assert block.mining(init=0, maximum_iter=100) is False

    assert block.hash_satisfies_difficulty is False
    assert block.is_valid is False
class BlockChain:
    """BlockChain object"""
    def __init__(self, block=None):
        """BlockChain object"""

        # Difficulty update parameters
        # 59 Seconds - the minimum interval between blocks is a minute
        self.minimum_interval = 59

        # 10 minutes - the objective interval between blocks
        self.block_interval = 600

        # 144 block - the time for evaluate intervals (one per day 6 * 24)
        self.difficulty_interval = 144

        # Currency values
        self.__unspent = None
        self.amount_mining = 0

        # Validate the inputs
        if block is None:
            self.__candidate = Block.genesis_block()
        elif isinstance(block, Block):
            self.__candidate = block
        else:
            raise Exception("The input parameter must be a Block object")

        if self.__candidate.is_valid:
            self.chain = [self.__candidate]
            self.__candidate = None
        else:
            self.chain = []

    def __repr__(self):
        """ Return repr(self). """

        last_block = max(0, self.num_blocks - 5)
        result = ""

        for step in reversed(range(last_block, self.num_blocks)):
            result = "%sBlock: %d (%s) - Hash: %s\n" % \
                     (result, self.chain[step].index, self.chain[step].timestamp,
                      self.chain[step].hash)

        if last_block > 0:
            result = '%s\nand %d block hidden' % (result, last_block)

        return result

    @property
    def is_valid(self):
        """Indicate if the blockchain is valid

        The blockchain is valid when
            1) The blocks are valid
            2) All blocks has previous block hash and a valid id
            3) All blocks are valid and

        Return:
             (Logical): True if BlockChain is valid
        """

        if self.chain:
            for step in range(self.num_blocks - 1, 0, -1):
                if not self.chain[step].is_valid:
                    return False
                elif self.chain[step].previous_hash != self.chain[step -
                                                                  1].hash:
                    return False
                elif self.chain[step].index != self.chain[step - 1].index + 1:
                    return False

            return self.chain[0].is_valid

        return False

    @property
    def candidate_block(self):
        """Return the candidate for block for the chain"""

        return self.__candidate

    @property
    def last_block(self):
        """Return the last block in the BlockChain"""

        if self.chain:
            return self.chain[-1]

        return None

    @property
    def num_blocks(self):
        """Return the number of blocks in the chain"""

        return len(self.chain)

    def add_candidate(self, data, timestamp=None, proof=0):
        """Insert a new candidate in the chain

        Return:
            (logical): true where the candidate can be assigned
        """

        if self.__candidate is not None:
            return False
        else:
            # Get the timestamp value where it is None
            if timestamp is None:
                timestamp = datetime.now()

            # Validate minimum timestamp period
            interval = timestamp - self.last_block.timestamp
            if interval.total_seconds() < self.minimum_interval:
                return False

            # Evaluate the difficulty
            if self.num_blocks > self.difficulty_interval and self.num_blocks % self.difficulty_interval == 0:
                interval = self.chain[-1].timestamp - self.chain[
                    -self.difficulty_interval - 1].timestamp
                interval = interval.total_seconds() / self.difficulty_interval

                if interval > self.block_interval:
                    difficulty = self.last_block.difficulty - 1
                elif interval < self.block_interval:
                    difficulty = self.last_block.difficulty + 1

            else:
                difficulty = self.last_block.difficulty

            self.__candidate = Block(self.last_block.index + 1,
                                     data,
                                     previous_hash=self.last_block.hash,
                                     timestamp=timestamp,
                                     proof=proof,
                                     difficulty=difficulty)

            return True

    def add_transaction(self, key, address, amount):
        """Add a transaction in the blockchain

        Add a transaction to the candidate list from the signer's account to the destination account for the indicated
        amount.

        Args:
            key (String): the private key of the user
            address (String): the destination address
            amount (Double): the amount of the

        Return:
            (Boolean): True if the transaction can be done
        """
        wallet = self.get_wallet(key)
        transaction = wallet.generate_transaction_to(address, amount)

        if transaction is None:
            return False

        unspent = self.get_unspent_list()

        return unspent.append_unconfirmed(transaction)

    def candidate_proof(self, proof):
        """Set the proof for a candidate"""

        self.__candidate.proof = proof

        if self.__candidate.is_valid:
            self.chain.append(self.__candidate)
            self.__candidate = None
            self.__unspent = None
            return True

        return False

    def generate_candidate(self, address, timestamp=None, proof=0):
        """Generate a new candidate block with a reward to the miner

        Args:
            address (String): the address fo the miner
            timestamp (String): the genesis block time
            proof (Integer): the proof

        Return:
            (logical): true where the candidate can be assigned
        """

        output = OutputTransaction(address, self.amount_mining)
        transaction = Transaction(timestamp, output)

        self.__unspent.append_unconfirmed(transaction)
        data = self.__unspent.unconfirmed

        return self.add_candidate(data, timestamp=timestamp, proof=proof)

    def get_unspent_list(self):
        """Get the list of unspent transaction

        Return:
            (UnspentList): the list unspent transactions
        """

        if self.__unspent is None:
            self.__unspent = UnspentList()

            for block in self.chain:
                for transaction in block.data:
                    assert self.__unspent.append_unconfirmed(transaction)

                if block.is_valid:
                    self.__unspent.confirm_unconfirmed()

        return self.__unspent

    def get_wallet(self, key=None):
        """Get the wallet of a user

        Args:
            key (String): the private key of the user

        Return:
             (Wallet): the wallet of the user
        """

        return Wallet(key, self)

    def mining_candidate(self, init=None, maximum_iter=1000):
        """Mining the Candidate Block

        Implements the search of an integer for the proof which satisficed the
        required difficult. The initial value can be configured using the
        `init` property. The maximum number of values to tried can be also
        configured using the `init` property.

        Args:
            self (Block): A Block object
            init (Integer): The values to use in the first proof
            maximum_iter (Integer): The maximum number of iterations in the mining process

        Return:
             (Logical): True if a valid proof has been found
        """

        if self.__candidate is None:
            return False

        if self.__candidate.mining(init, maximum_iter):
            self.chain.append(self.__candidate)
            self.__candidate = None
            self.__unspent = None
            return True

        return False

    @staticmethod
    def new_cryptocurrency(address,
                           amount,
                           timestamp=None,
                           proof=0,
                           difficulty=0,
                           mining=False):
        """Create a new cryptocurrency

        Args:
            address (string): the address of the first user
            amount (Double): the amount for the first user
            timestamp (String): the genesis block time
            proof (Integer): the proof
            difficulty (Integer): the number of zeros in the hash to validate the block
            mining (Boolean): logical value indicating if the block must be mined

        Return:
            (BlockChain): A new blockchain
        """

        output = OutputTransaction(address, amount)
        transaction = Transaction(timestamp, output)
        block = Block.genesis_block([transaction],
                                    timestamp=timestamp,
                                    proof=proof,
                                    difficulty=difficulty,
                                    mining=mining)

        blokchain = BlockChain(block)
        blokchain.amount_mining = amount

        return blokchain

    def replace_chain(self, new_chain):
        """Replace this chair for a longest one

        Return:
            (logical): True is the replacement is valid
        """

        if isinstance(new_chain, BlockChain):
            if new_chain.num_blocks > self.num_blocks:
                if new_chain.chain[0] == self.chain[0]:
                    if new_chain.is_valid:
                        self.chain = new_chain.chain
                        self.__candidate = new_chain.candidate_block

                        return True

        return False