@Test
public void chainDownloadEnd2End() throws Exception {

    // A full end-to-end test of the chain download process, with a new block being solved in the middle.
    Block b1 = createFakeBlock(blockStore, Block.BLOCK_HEIGHT_GENESIS).block;
    blockChain.add(b1);
    Block b2 = makeSolvedTestBlock(b1);
    Block b3 = makeSolvedTestBlock(b2);
    Block b4 = makeSolvedTestBlock(b3);
    Block b5 = makeSolvedTestBlock(b4);
    connect();
    
    peer.startBlockChainDownload();
    GetBlocksMessage getblocks = (GetBlocksMessage)outbound(writeTarget);
    assertEquals(blockStore.getChainHead().getHeader().getHash(), getblocks.getLocator().get(0));
    assertEquals(Sha256Hash.ZERO_HASH, getblocks.getStopHash());
    // Remote peer sends us an inv with some blocks.
    InventoryMessage inv = new InventoryMessage(PARAMS);
    inv.addBlock(b2);
    inv.addBlock(b3);
    // We do a getdata on them.
    inbound(writeTarget, inv);
    GetDataMessage getdata = (GetDataMessage)outbound(writeTarget);
    assertEquals(b2.getHash(), getdata.getItems().get(0).hash);
    assertEquals(b3.getHash(), getdata.getItems().get(1).hash);
    assertEquals(2, getdata.getItems().size());
    // Remote peer sends us the blocks. The act of doing a getdata for b3 results in getting an inv with just the
    // best chain head in it.
    inbound(writeTarget, b2);
    inbound(writeTarget, b3);
    inv = new InventoryMessage(PARAMS);
    inv.addBlock(b5);
    // We request the head block.
    inbound(writeTarget, inv);
    getdata = (GetDataMessage)outbound(writeTarget);
    assertEquals(b5.getHash(), getdata.getItems().get(0).hash);
    assertEquals(1, getdata.getItems().size());
    // Peer sends us the head block. The act of receiving the orphan block triggers a getblocks to fill in the
    // rest of the chain.
    inbound(writeTarget, b5);
    getblocks = (GetBlocksMessage)outbound(writeTarget);
    assertEquals(b5.getHash(), getblocks.getStopHash());
    assertEquals(b3.getHash(), getblocks.getLocator().get(0));
    // At this point another block is solved and broadcast. The inv triggers a getdata but we do NOT send another
    // getblocks afterwards, because that would result in us receiving the same set of blocks twice which is a
    // timewaste. The getblocks message that would have been generated is set to be the same as the previous
    // because we walk backwards down the orphan chain and then discover we already asked for those blocks, so
    // nothing is done.
    Block b6 = makeSolvedTestBlock(b5);
    inv = new InventoryMessage(PARAMS);
    inv.addBlock(b6);
    inbound(writeTarget, inv);
    getdata = (GetDataMessage)outbound(writeTarget);
    assertEquals(1, getdata.getItems().size());
    assertEquals(b6.getHash(), getdata.getItems().get(0).hash);
    inbound(writeTarget, b6);
    assertNull(outbound(writeTarget));  // Nothing is sent at this point.
    // We're still waiting for the response to the getblocks (b3,b5) sent above.
    inv = new InventoryMessage(PARAMS);
    inv.addBlock(b4);
    inv.addBlock(b5);
    inbound(writeTarget, inv);
    getdata = (GetDataMessage)outbound(writeTarget);
    assertEquals(1, getdata.getItems().size());
    assertEquals(b4.getHash(), getdata.getItems().get(0).hash);
    // We already have b5 from before, so it's not requested again.
    inbound(writeTarget, b4);
    assertNull(outbound(writeTarget));
    // b5 and b6 are now connected by the block chain and we're done.
    assertNull(outbound(writeTarget));
    closePeer(peer);
}