static void AddConsumers(ConversationThread conversation, ChartTable chart, OutputTimelineOptions options)
        {
            foreach (var consumer in conversation.Consumers.OrderBy(x => x.Message.StartTime))
            {
                var sb = new StringBuilder(60);
                if (conversation.Depth > 1)
                {
                    sb.Append(' ', (conversation.Depth - 1) * 2);
                }
                if (conversation.Depth > 0)
                {
                    sb.Append("\x2514 ");
                }

                sb.Append("Consume ");
                sb.Append(options.MessageType(consumer.Message));

                chart.Add(sb.ToString(), consumer.Message.StartTime, consumer.Message.ElapsedTime, consumer.Message.Address);
            }
        }
        /// <summary>
        /// Output a timeline of messages published, sent, and consumed by the test harness.
        /// </summary>
        /// <param name="harness"></param>
        /// <param name="textWriter"></param>
        /// <param name="configure">Configure the timeout output options</param>
        /// <exception cref="ArgumentNullException"></exception>
        public static async Task OutputTimeline(this BusTestHarness harness, TextWriter textWriter, Action <OutputTimelineOptions> configure = default)
        {
            if (harness == null)
            {
                throw new ArgumentNullException(nameof(harness));
            }
            if (textWriter == null)
            {
                throw new ArgumentNullException(nameof(textWriter));
            }

            var options = new OutputTimelineOptions();

            configure?.Invoke(options);

            options.Apply(harness);

            await harness.InactivityTask.ConfigureAwait(false);

            var produced = new List <Message>();

            await foreach (var message in harness.Published.SelectAsync(_ => true).ConfigureAwait(false))
            {
                produced.Add(new Message(message));
            }

            await foreach (var message in harness.Sent.SelectAsync(_ => true).ConfigureAwait(false))
            {
                produced.Add(new Message(message));
            }

            var consumed = new List <Message>();

            await foreach (var message in harness.Consumed.SelectAsync(_ => true).ConfigureAwait(false))
            {
                consumed.Add(new Message(message));
            }

            Dictionary <Guid?, ConversationThread> conversations = produced.GroupBy(m => m.ConversationId).Select(x =>
            {
                List <Message> messages = x.OrderBy(m => m.StartTime).ToList();

                var initiator = messages.FirstOrDefault(m => m.ParentMessageId == default) ?? messages.First();

                var initiatorThread = new ConversationThread(initiator, 1);

                var stack = new Stack <ConversationThread>();
                stack.Push(initiatorThread);

                while (stack.Any())
                {
                    var thread = stack.Pop();

                    List <Message> consumes = consumed.Where(message => message.MessageId == thread.Message.MessageId).ToList();
                    thread.Consumers.AddRange(consumes.Select(message => new ConversationConsumer(message)));

                    IEnumerable <Message> threadMessages = x.Where(m => m.ParentMessageId == thread.Message.MessageId);
                    foreach (var message in threadMessages)
                    {
                        var nextThread = new ConversationThread(message, thread.Depth + 1);
                        thread.Nodes.Add(nextThread);
                        stack.Push(nextThread);
                    }
                }

                return(initiatorThread);
            }).ToDictionary(x => x.Message.ConversationId);

            var chart = new ChartTable();

            foreach (var conversation in conversations.Values.OrderBy(x => x.Message.StartTime))
            {
                var whitespace       = new string(' ', (conversation.Depth - 1) * 2);
                var conversationLine = $"{whitespace}{conversation.Message.EventType} {options.MessageType(conversation.Message)}";

                chart.Add(conversationLine, conversation.Message.StartTime, conversation.Message.ElapsedTime, conversation.Message.Address);

                AddConsumers(conversation, chart, options);

                var stack = new Stack <ConversationThread>(conversation.Nodes.OrderByDescending(x => x.Message.StartTime));
                while (stack.Any())
                {
                    var current = stack.Pop();

                    whitespace = new string(' ', (current.Depth - 1) * 2);
                    var line = $"{whitespace}{current.Message.EventType} {options.MessageType(current.Message)}";

                    chart.Add(line, current.Message.StartTime, current.Message.ElapsedTime, current.Message.Address);

                    AddConsumers(current, chart, options);

                    foreach (var node in current.Nodes.OrderByDescending(x => x.Message.StartTime))
                    {
                        stack.Push(node);
                    }
                }
            }

            options.GetTable(chart)
            .SetColumn(1, "Duration", typeof(int))
            .SetRightNumberAlignment()
            .OutputTo(textWriter)
            .Write();
        }