/*
 * 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.net.eth.handler;

import org.ethereum.config.CommonConfig;
import org.ethereum.config.NoAutoscan;
import org.ethereum.config.SystemProperties;
import org.ethereum.core.Block;
import org.ethereum.core.BlockHeader;
import org.ethereum.core.Blockchain;
import org.ethereum.core.BlockchainImpl;
import org.ethereum.db.BlockStore;
import org.ethereum.db.BlockStoreDummy;
import org.ethereum.listener.CompositeEthereumListener;
import org.ethereum.net.eth.message.EthMessage;
import org.ethereum.net.eth.message.GetBlockBodiesMessage;
import org.ethereum.net.eth.message.GetBlockHeadersMessage;
import org.ethereum.sync.SyncManager;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.mockito.Mockito;
import org.spongycastle.util.encoders.Hex;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;

import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Testing whether Eth handler {@link Eth62} is blocking {@link BlockchainImpl}
 */
public class LockBlockchainTest {

    private final AtomicBoolean result = new AtomicBoolean();
    private Blockchain blockchain;

    private final static long DELAY = 1000;  //Default delay in ms
    private final static String BLOCK_RLP = "f901f8f901f3a00000000000000000000000000000000000000000000000000000000000000000a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347940000000000000000000000000000000000000000a09178d0f23c965d81f0834a4c72c6253ce6830f4022b1359aaebfc1ecba442d4ea056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008302000080832fefd8808080a0000000000000000000000000000000000000000000000000000000000000000088000000000000002ac0c0";

    public LockBlockchainTest() {

        SysPropConfig1.props.overrideParams(
                "peer.discovery.enabled", "false",
                "peer.listen.port", "37777",
                "peer.privateKey", "3ec771c31cac8c0dba77a69e503765701d3c2bb62435888d4ffa38fed60c445c",
                "genesis", "genesis-light.json",
                "database.dir", "testDB-1");

        final BlockStore blockStoreDummy = new BlockStoreDummy() {
            @Override
            public synchronized Block getChainBlockByNumber(long blockNumber) {
                return super.getChainBlockByNumber(blockNumber);
            }

            @Override
            public synchronized List<BlockHeader> getListHeadersEndWith(byte[] hash, long qty) {
                return super.getListHeadersEndWith(hash, qty);
            }

            @Override
            public synchronized Block getBestBlock() {
                return new Block(Hex.decode(BLOCK_RLP));
            }
        };

        this.blockchain = new BlockchainImpl(SysPropConfig1.props) {
            @Override
            public synchronized boolean isBlockExist(byte[] hash) {
                try {
                    this.blockStore = blockStoreDummy;
                    Thread.sleep(100000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return true;
            }
        };

        SysPropConfig1.testHandler = new Eth62(SysPropConfig1.props, blockchain, blockStoreDummy,
                new CompositeEthereumListener()) {
            {
                this.blockstore = blockStoreDummy;
                this.syncManager = Mockito.mock(SyncManager.class);
            }
            @Override
            public synchronized void sendStatus() {
                super.sendStatus();
            }

            @Override
            protected void sendMessage(EthMessage message) {
                result.set(true);
                System.out.println("Mocking message sending...");
            }
        };
    }

    @Before
    public void setUp() throws Exception {
        result.set(false);
    }

    @Configuration
    @NoAutoscan
    public static class SysPropConfig1 {
        static Eth62 testHandler = null;

        @Bean
        @Scope("prototype")
        public Eth62 eth62() {
            return testHandler;
        }

        static SystemProperties props = new SystemProperties();

        @Bean
        public SystemProperties systemProperties() {
            return props;
        }
    }

    @Test
    public synchronized void testHeadersWithoutSkip() throws FileNotFoundException, InterruptedException {
        ExecutorService executor1 = Executors.newSingleThreadExecutor();
        executor1.submit(() -> {
            blockchain.isBlockExist(null);
        }
        );
        this.wait(DELAY);
        ExecutorService executor2 = Executors.newSingleThreadExecutor();
        executor2.submit(() -> {
            GetBlockHeadersMessage msg = new GetBlockHeadersMessage(1L, new byte[0], 10, 0, false);
            SysPropConfig1.testHandler.processGetBlockHeaders(msg);
        }
        );
        this.wait(DELAY);
        assert result.get();
    }

    @Test
    public synchronized void testHeadersWithSkip() throws FileNotFoundException, InterruptedException {
        ExecutorService executor1 = Executors.newSingleThreadExecutor();
        executor1.submit(() -> {
            blockchain.isBlockExist(null);
        }
        );
        this.wait(DELAY);
        ExecutorService executor2 = Executors.newSingleThreadExecutor();
        executor2.submit(() -> {
            GetBlockHeadersMessage msg = new GetBlockHeadersMessage(1L, new byte[0], 10, 5, false);
            SysPropConfig1.testHandler.processGetBlockHeaders(msg);
        }
        );
        this.wait(DELAY);
        assert result.get();
    }

    @Test
    public synchronized void testBodies() throws FileNotFoundException, InterruptedException {
        ExecutorService executor1 = Executors.newSingleThreadExecutor();
        executor1.submit(() -> {
            blockchain.isBlockExist(null);
        }
        );
        this.wait(DELAY);
        ExecutorService executor2 = Executors.newSingleThreadExecutor();
        executor2.submit(() -> {
            List<byte[]> hashes = new ArrayList<>();
            hashes.add(new byte[] {1, 2, 3});
            hashes.add(new byte[] {4, 5, 6});
            GetBlockBodiesMessage msg = new GetBlockBodiesMessage(hashes);
            SysPropConfig1.testHandler.processGetBlockBodies(msg);
        }
        );
        this.wait(DELAY);
        assert result.get();
    }

    @Test
    public synchronized void testStatus() throws FileNotFoundException, InterruptedException {
        ExecutorService executor1 = Executors.newSingleThreadExecutor();
        executor1.submit(() -> {
            blockchain.isBlockExist(null);
        }
        );
        this.wait(DELAY);
        ExecutorService executor2 = Executors.newSingleThreadExecutor();
        executor2.submit(() -> {
            try {
                SysPropConfig1.testHandler.sendStatus();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        );
        this.wait(DELAY);
        assert result.get();
    }
}