/*
 * Copyright (c) [2016] [ <ether.camp> ]
 * This file is part of the ethereumJ library.
 *
 * The ethereumJ library is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * The ethereumJ library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with the ethereumJ library. If not, see <http://www.gnu.org/licenses/>.
 */
package org.ethereum.core;

import org.ethereum.datasource.inmem.HashMapDB;
import org.ethereum.db.RepositoryRoot;
import org.ethereum.db.BlockStoreDummy;
import org.junit.Test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Testing {@link BlockchainImpl#getListOfHeadersStartFrom(BlockIdentifier, int, int, boolean)}
 */
public class BlockchainGetHeadersTest {

    private class BlockStoreMock extends BlockStoreDummy {

        private List<Block> dummyBlocks = new ArrayList<>();

        public BlockStoreMock() {
            byte [] emptyArray = new byte[0];
            byte [] recentHash = emptyArray;

            for (long i = 0; i < 10; ++i) {
                BlockHeader blockHeader = new BlockHeader(recentHash, emptyArray, emptyArray, emptyArray, emptyArray,
                        i, emptyArray, 0L, 0L, emptyArray, emptyArray, emptyArray);
                recentHash = blockHeader.getHash();
                Block block = new Block(blockHeader, new ArrayList<Transaction>(), new ArrayList<BlockHeader>());
                dummyBlocks.add(block);
            }
        }

        @Override
        public Block getBlockByHash(byte[] hash) {
            for (Block block: dummyBlocks) {
                if (Arrays.equals(block.getHash(), hash)) {
                    return block;
                }
            }

            return null;
        }

        @Override
        public Block getChainBlockByNumber(long blockNumber) {
            return blockNumber < dummyBlocks.size() ? dummyBlocks.get((int) blockNumber) : null;
        }

        @Override
        public List<BlockHeader> getListHeadersEndWith(byte[] hash, long qty) {
            List<BlockHeader> headers = new ArrayList<>();
            Block start = getBlockByHash(hash);
            if (start != null) {
                long i = start.getNumber();
                while (i >= 0 && headers.size() < qty) {
                    headers.add(getChainBlockByNumber(i).getHeader());
                    --i;
                }
            }

            return headers;
        }

        @Override
        public Block getBestBlock() {
            return dummyBlocks.get(dummyBlocks.size() - 1);
        }
    }


    private class BlockchainImplTester extends BlockchainImpl {

        public BlockchainImplTester() {
            blockStore = new BlockStoreMock();
            setRepository(new RepositoryRoot(new HashMapDB<byte[]>()));
            setBestBlock(blockStore.getChainBlockByNumber(9));
        }
    }


    private BlockchainImpl blockchain;

    public BlockchainGetHeadersTest() {
        blockchain = new BlockchainImplTester();
    }

    @Test
    public void singleHeader() {
        // Get by number
        long blockNumber = 2L;
        BlockIdentifier identifier = new BlockIdentifier(null, blockNumber);
        List<BlockHeader> headers = blockchain.getListOfHeadersStartFrom(identifier, 0, 1, false);

        assert headers.size() == 1;
        assert headers.get(0).getNumber() == blockNumber;

        // Get by hash
        byte[] hash = headers.get(0).getHash();
        BlockIdentifier hashIdentifier = new BlockIdentifier(hash, 0L);
        List<BlockHeader> headersByHash = blockchain.getListOfHeadersStartFrom(hashIdentifier, 0, 1, false);

        assert headersByHash.size() == 1;
        assert headersByHash.get(0).getNumber() == blockNumber;

        // Reverse doesn't matter for single block
        List<BlockHeader> headersReverse = blockchain.getListOfHeadersStartFrom(hashIdentifier, 0, 1, true);
        assert headersReverse.size() == 1;
        assert headersReverse.get(0).getNumber() == blockNumber;

        // Skip doesn't matter for single block
        List<BlockHeader> headersSkip = blockchain.getListOfHeadersStartFrom(hashIdentifier, 15, 1, false);
        assert headersSkip.size() == 1;
        assert headersSkip.get(0).getNumber() == blockNumber;
    }

    @Test
    public void continuousHeaders() {
        // Get by number
        long blockNumber = 2L;
        BlockIdentifier identifier = new BlockIdentifier(null, blockNumber);
        List<BlockHeader> headers = blockchain.getListOfHeadersStartFrom(identifier, 0, 3, false);

        assert headers.size() == 3;
        assert headers.get(0).getNumber() == blockNumber;
        assert headers.get(1).getNumber() == blockNumber + 1;
        assert headers.get(2).getNumber() == blockNumber + 2;

        List<BlockHeader> headersReverse = blockchain.getListOfHeadersStartFrom(identifier, 0, 3, true);
        assert headersReverse.size() == 3;
        assert headersReverse.get(0).getNumber() == blockNumber;
        assert headersReverse.get(1).getNumber() == blockNumber - 1;
        assert headersReverse.get(2).getNumber() == blockNumber - 2;

        // Requesting more than we have
        BlockIdentifier identifierMore = new BlockIdentifier(null, 8L);
        List<BlockHeader> headersMore = blockchain.getListOfHeadersStartFrom(identifierMore, 0, 3, false);
        assert headersMore.size() == 2;
        assert headersMore.get(0).getNumber() == 8L;
        assert headersMore.get(1).getNumber() == 9L;
    }

    @Test
    public void gapedHeaders() {
        int skip = 2;
        BlockIdentifier identifier = new BlockIdentifier(null, 2L);
        List<BlockHeader> headers = blockchain.getListOfHeadersStartFrom(identifier, skip, 3, false);

        assert headers.size() == 3;
        assert headers.get(0).getNumber() == 2L;
        assert headers.get(1).getNumber() == 5L; // 2, [3, 4], 5 - skipping []
        assert headers.get(2).getNumber() == 8L; // 5, [6, 7], 8 - skipping []

        // Same for reverse
        BlockIdentifier identifierReverse = new BlockIdentifier(null, 8L);
        List<BlockHeader> headersReverse = blockchain.getListOfHeadersStartFrom(identifierReverse, skip, 3, true);
        assert headersReverse.size() == 3;
        assert headersReverse.get(0).getNumber() == 8L;
        assert headersReverse.get(1).getNumber() == 5L;
        assert headersReverse.get(2).getNumber() == 2L;

        // Requesting more than we have
        BlockIdentifier identifierMore = new BlockIdentifier(null, 8L);
        List<BlockHeader> headersMore = blockchain.getListOfHeadersStartFrom(identifierMore, skip, 3, false);
        assert headersMore.size() == 1;
        assert headersMore.get(0).getNumber() == 8L;
    }

    @Test
    public void closeToBounds() {
        int skip = 1;
        BlockIdentifier identifier = new BlockIdentifier(null, 2L);
        List<BlockHeader> headers = blockchain.getListOfHeadersStartFrom(identifier, skip, 3, true);

        assert headers.size() == 2;
        assert headers.get(0).getNumber() == 2L;
        assert headers.get(1).getNumber() == 0L;

        BlockIdentifier identifier2 = new BlockIdentifier(null, 0L);
        List<BlockHeader> headers2 = blockchain.getListOfHeadersStartFrom(identifier2, skip, 3, true);

        assert headers2.size() == 1;
        assert headers2.get(0).getNumber() == 0L;

        BlockIdentifier identifier3 = new BlockIdentifier(null, 0L);
        List<BlockHeader> headers3 = blockchain.getListOfHeadersStartFrom(identifier3, skip, 1, true);

        assert headers3.size() == 1;
        assert headers3.get(0).getNumber() == 0L;

        BlockIdentifier identifier4 = new BlockIdentifier(null, blockchain.getBestBlock().getNumber());
        List<BlockHeader> headers4 = blockchain.getListOfHeadersStartFrom(identifier4, skip, 3, false);

        assert headers4.size() == 1;
        assert headers4.get(0).getNumber() == blockchain.getBestBlock().getNumber();

        BlockIdentifier identifier5 = new BlockIdentifier(null, blockchain.getBestBlock().getNumber() - 1);
        List<BlockHeader> headers5 = blockchain.getListOfHeadersStartFrom(identifier5, 0, 3, false);

        assert headers5.size() == 2;
        assert headers5.get(0).getNumber() == blockchain.getBestBlock().getNumber() - 1;
        assert headers5.get(1).getNumber() == blockchain.getBestBlock().getNumber();
    }
}
