public async Task Complete()
        {
            // 0, 1: Already existed blocks
            // 2, 3, 4,  5,  6: first chunk
            // 7, 8, 9, 10, 11: second chunk
            //   12,    13, 14: last chunk
            ImmutableArray <Block <DumbAction> > fixture =
                GenerateBlocks <DumbAction>(15).ToImmutableArray();

            // Blocks each block has:
            //   A: 0, 4, 8,  12
            //   B: 1, 5, 9,  13
            //   C: 2, 6, 10, 14
            //   D: 3, 7, 11
            char[] peers = { 'A', 'B', 'C', 'D' };
            ImmutableDictionary <char, ImmutableDictionary <HashDigest <SHA256>, Block <DumbAction> > >
            peerBlocks = peers.ToImmutableDictionary(
                p => p,
                p => fixture.Skip(p - 'A').Where((_, i) => i % 4 < 1).ToImmutableDictionary(
                    b => b.Hash,
                    b => b
                    )
                );

            const int initialHeight = 2;
            const int window        = 5;
            var       bc            = new BlockCompletion <char, DumbAction>(
                fixture.Take(initialHeight).Select(b => b.Hash).ToImmutableHashSet().Contains,
                window
                );
            ImmutableArray <HashDigest <SHA256> > initialDemands = fixture
                                                                   .Skip(initialHeight + 10)
                                                                   .Select(b => b.Hash)
                                                                   .ToImmutableArray();

            bc.Demand(initialDemands);
            _logger.Verbose("Initial demands: {0}", initialDemands);
            IAsyncEnumerable <Tuple <Block <DumbAction>, char> > rv = bc.Complete(
                new[] { 'A', 'B', 'C', 'D' },
                (peer, hashes, token) => new AsyncEnumerable <Block <DumbAction> >(async yield =>
            {
                var blocksPeerHas = peerBlocks[peer];
                var sent          = new HashSet <HashDigest <SHA256> >();
                foreach (HashDigest <SHA256> hash in hashes)
                {
                    if (blocksPeerHas.ContainsKey(hash))
                    {
                        Block <DumbAction> block = blocksPeerHas[hash];
                        await yield.ReturnAsync(block);
                        sent.Add(block.Hash);
                    }
                }

                _logger.Verbose("Peer {Peer} sent blocks: {SentBlockHashes}.", peer, sent);
            })
                );

            var downloadedBlocks = new HashSet <Block <DumbAction> >();
            var sourcePeers      = new HashSet <char>();
            await AsyncEnumerable.ForEachAsync(rv, pair =>
            {
                downloadedBlocks.Add(pair.Item1);
                sourcePeers.Add(pair.Item2);
            });

            Assert.Equal(fixture.Skip(2).ToHashSet(), downloadedBlocks);
            Assert.Subset(peers.ToHashSet(), sourcePeers);
        }
        public async void EnumerateChunks()
        {
            // 0, 1: Already existed blocks
            // 2, 3, 4,  5,  6: first chunk
            // 7, 8, 9, 10, 11: second chunk
            //   12,    13, 14: last chunk
            ImmutableArray <Block <DumbAction> > fixture =
                GenerateBlocks <DumbAction>(15).ToImmutableArray();
            const int initialHeight = 2;
            const int window        = 5;
            var       bc            = new BlockCompletion <int, DumbAction>(
                fixture.Take(initialHeight).Select(b => b.Hash).ToImmutableHashSet().Contains,
                window
                );
            var logs = new ConcurrentBag <(int, ImmutableArray <HashDigest <SHA256> >)>();
            var ev   = new AsyncAutoResetEvent(false);
            var bg   = Task.Run(async() =>
            {
                await Task.Delay(100);
                int i = 0;
                await AsyncEnumerable.ForEachAsync(bc.EnumerateChunks(), hashes =>
                {
                    ImmutableArray <HashDigest <SHA256> > hashesArray = hashes.ToImmutableArray();
                    logs.Add((i, hashesArray));
                    i++;

                    // To test dynamic demands
                    if (hashesArray.Contains(fixture[7].Hash))
                    {
                        bc.Demand(fixture[14].Hash);
                        bc.Demand(fixture[0].Hash);  // This should be ignored as it's existed.
                        bc.Demand(fixture[3].Hash);  // This should be ignored as it's satisfied.
                    }

                    ev.Set();
                    _logger.Verbose("Got a chunk of hashes: {0}", string.Join(", ", hashesArray));
                });
            });

            // Demand: 2, 3, 4, 5, 6
            bc.Demand(fixture.Skip(initialHeight).Take(5).Select(b => b.Hash));

            // Chunk: 2, 3, 4, 5, 6
            _logger.Verbose("Waiting demand #2-6...");

            // TODO change waiting condition
            await Task.Delay(1000);

            _logger.Verbose("Demand #2-6 processed.");
            var actual = new List <HashDigest <SHA256> >();

            while (logs.TryTake(out var log))
            {
                actual.AddRange(log.Item2);
            }

            Assert.Equal(fixture.Skip(initialHeight).Take(window).Select(b => b.Hash), actual);

            // Complete: 2, 3, 4, 5 (and no 6)
            for (int i = initialHeight; i < initialHeight + window - 1; i++)
            {
                bc.Satisfy(fixture[i]);
            }

            // Demand: 7, 8, 9, 10, 11, 12, 13 (and 14 <- 7 will be added soon)
            bc.Demand(fixture.Skip(initialHeight + window).Select(b => b.Hash));

            // Chunk: 7, 8, 9, 10, 11
            _logger.Verbose("Waiting demand #7-11...");
            // TODO change waiting condition
            await Task.Delay(1000);

            _logger.Verbose("Demand #7-11 processed.");

            actual = new List <HashDigest <SHA256> >();
            while (logs.TryTake(out var log))
            {
                actual.AddRange(log.Item2);
            }

            Assert.Equal(
                fixture.Skip(initialHeight + window).Take(window).Select(b => b.Hash),
                actual
                );

            // Complete: 6, 7, 8, 9, 10, 11
            for (int i = initialHeight + window - 1; i < initialHeight + window * 2; i++)
            {
                bc.Satisfy(fixture[i]);
            }

            // Chunk: 12, 13, 14
            _logger.Verbose("Waiting demand #12-14...");
            // TODO change waiting condition
            await Task.Delay(1000);

            _logger.Verbose("Demand #12-14 processed.");
            actual = new List <HashDigest <SHA256> >();
            while (logs.TryTake(out var log))
            {
                actual.AddRange(log.Item2);
            }

            Assert.Equal(
                fixture.Skip(initialHeight + window * 2).Select(b => b.Hash).ToImmutableHashSet(),
                actual.ToImmutableHashSet()
                );

            // Complete: 12, 13, 14
            for (int i = initialHeight + window * 2; i < fixture.Length; i++)
            {
                bc.Satisfy(fixture[i]);
            }

            await bg;
        }