use std::io;
use std::ops::Range;

use crate::{IndexType, LabelDisplay};

use super::draw::{self, StreamAwareFmt, StreamType};
use super::{Cache, CharSet, LabelAttach, Report, ReportKind, Show, Span, Write};

// A WARNING, FOR ALL YE WHO VENTURE IN HERE
//
// - This code is complex and has a lot of implicit invariants
// - Yes, it has some bugs
// - Yes, it needs rewriting
// - No, you are not expected to understand it. I will probably not understand it either in a month, but that will only
//   give me a reason to rewrite it

enum LabelKind {
    Inline,
    Multiline,
}

struct LabelInfo<'a> {
    kind: LabelKind,
    char_span: Range<usize>,
    display_info: &'a LabelDisplay,
    line: usize,
}

impl<'a> LabelInfo<'a> {
    fn last_offset(&self) -> usize {
        self.char_span
            .end
            .saturating_sub(1)
            .max(self.char_span.start)
    }
}

struct SourceGroup<'a, S: Span> {
    src_id: &'a S::SourceId,
    char_span: Range<usize>,
    labels: Vec<LabelInfo<'a>>,
}

impl<S: Span> Report<'_, S> {
    fn get_source_groups(&self, cache: &mut impl Cache<S::SourceId>) -> Vec<SourceGroup<S>> {
        let mut labels = Vec::new();
        for label in self.labels.iter() {
            let label_source = label.span.source();

            let src_display = cache.display(label_source);
            let src = match cache.fetch(label_source) {
                Ok(src) => src,
                Err(e) => {
                    eprintln!("Unable to fetch source '{}': {:?}", Show(src_display), e);
                    continue;
                }
            };

            let given_label_span = label.span.start()..label.span.end();

            let (label_char_span, start_line, end_line) = match self.config.index_type {
                IndexType::Char => {
                    let Some(start_line) = src.get_offset_line(given_label_span.start) else {
                        continue;
                    };
                    let end_line = if given_label_span.start >= given_label_span.end {
                        start_line.1
                    } else {
                        let Some(end_line) = src.get_offset_line(given_label_span.end - 1) else {
                            continue;
                        };
                        end_line.1
                    };
                    (given_label_span, start_line.1, end_line)
                }
                IndexType::Byte => {
                    let Some((start_line_obj, start_line, start_byte_col)) =
                        src.get_byte_line(given_label_span.start)
                    else {
                        continue;
                    };
                    let line_text = src.get_line_text(start_line_obj).unwrap();

                    let num_chars_before_start = line_text[..start_byte_col.min(line_text.len())]
                        .chars()
                        .count();
                    let start_char_offset = start_line_obj.offset() + num_chars_before_start;

                    if given_label_span.start >= given_label_span.end {
                        (start_char_offset..start_char_offset, start_line, start_line)
                    } else {
                        // We can subtract 1 from end, because get_byte_line doesn't actually index into the text.
                        let end_pos = given_label_span.end - 1;
                        let Some((end_line_obj, end_line, end_byte_col)) =
                            src.get_byte_line(end_pos)
                        else {
                            continue;
                        };
                        let end_line_text = src.get_line_text(end_line_obj).unwrap();
                        // Have to add 1 back now, so we don't cut a char in two.
                        let num_chars_before_end =
                            end_line_text[..end_byte_col + 1].chars().count();
                        let end_char_offset = end_line_obj.offset() + num_chars_before_end;

                        (start_char_offset..end_char_offset, start_line, end_line)
                    }
                }
            };

            let label_info = LabelInfo {
                kind: if start_line == end_line {
                    LabelKind::Inline
                } else {
                    LabelKind::Multiline
                },
                char_span: label_char_span,
                display_info: &label.display_info,
                line: end_line,
            };

            labels.push((label_info, label_source));
        }
        labels.sort_by_key(|(l, _)| l.display_info.order);
        let mut groups = Vec::<SourceGroup<_>>::new();
        for (label, src_id) in labels {
            match groups.last_mut() {
                Some(group)
                    if group.src_id == src_id
                        && group
                            .labels
                            .last()
                            .map_or(true, |last| last.line <= label.line) =>
                {
                    group.char_span.start = group.char_span.start.min(label.char_span.start);
                    group.char_span.end = group.char_span.end.max(label.char_span.end);
                    group.labels.push(label);
                }
                _ => {
                    groups.push(SourceGroup {
                        src_id: src_id,
                        char_span: label.char_span.clone(),
                        labels: vec![label],
                    });
                }
            }
        }
        groups
    }

    /// Write this diagnostic to an implementor of [`Write`].
    ///
    /// If using the `concolor` feature, this method assumes that the output is ultimately going to be printed to
    /// `stderr`.  If you are printing to `stdout`, use the [`write_for_stdout`](Self::write_for_stdout) method instead.
    ///
    /// If you wish to write to `stderr` or `stdout`, you can do so via [`Report::eprint`] or [`Report::print`] respectively.
    pub fn write<C: Cache<S::SourceId>, W: Write>(&self, cache: C, w: W) -> io::Result<()> {
        self.write_for_stream(cache, w, StreamType::Stderr)
    }

    /// Write this diagnostic to an implementor of [`Write`], assuming that the output is ultimately going to be printed
    /// to `stdout`.
    pub fn write_for_stdout<C: Cache<S::SourceId>, W: Write>(
        &self,
        cache: C,
        w: W,
    ) -> io::Result<()> {
        self.write_for_stream(cache, w, StreamType::Stdout)
    }

    /// Write this diagnostic to an implementor of [`Write`], assuming that the output is ultimately going to be printed
    /// to the given output stream (`stdout` or `stderr`).
    fn write_for_stream<C: Cache<S::SourceId>, W: Write>(
        &self,
        mut cache: C,
        mut w: W,
        s: StreamType,
    ) -> io::Result<()> {
        let draw = match self.config.char_set {
            CharSet::Unicode => draw::Characters::unicode(),
            CharSet::Ascii => draw::Characters::ascii(),
        };

        // --- Header ---

        let code = self.code.as_ref().map(|c| format!("[{}] ", c));
        let id = format!("{}{}:", Show(code), self.kind);
        let kind_color = match self.kind {
            ReportKind::Error => self.config.error_color(),
            ReportKind::Warning => self.config.warning_color(),
            ReportKind::Advice => self.config.advice_color(),
            ReportKind::Custom(_, color) => Some(color),
        };
        writeln!(w, "{} {}", id.fg(kind_color, s), Show(self.msg.as_ref()))?;

        let groups = self.get_source_groups(&mut cache);

        // Line number maximum width
        let line_no_width = groups
            .iter()
            .filter_map(
                |SourceGroup {
                     char_span, src_id, ..
                 }| {
                    let src_name = cache
                        .display(src_id)
                        .map(|d| d.to_string())
                        .unwrap_or_else(|| "<unknown>".to_string());

                    let src = match cache.fetch(src_id) {
                        Ok(src) => src,
                        Err(e) => {
                            eprintln!("Unable to fetch source {}: {:?}", src_name, e);
                            return None;
                        }
                    };

                    let line_range = src.get_line_range(char_span);
                    Some(
                        (1..)
                            .map(|x| 10u32.pow(x))
                            .take_while(|x| line_range.end as u32 / x != 0)
                            .count()
                            + 1,
                    )
                },
            )
            .max()
            .unwrap_or(0);

        // --- Source sections ---
        let groups_len = groups.len();
        for (
            group_idx,
            SourceGroup {
                src_id,
                char_span,
                labels,
            },
        ) in groups.into_iter().enumerate()
        {
            let src_name = cache
                .display(src_id)
                .map(|d| d.to_string())
                .unwrap_or_else(|| "<unknown>".to_string());

            let src = match cache.fetch(src_id) {
                Ok(src) => src,
                Err(e) => {
                    eprintln!("Unable to fetch source {}: {:?}", src_name, e);
                    continue;
                }
            };

            let line_range = src.get_line_range(&char_span);

            // File name & reference
            let location = if src_id == self.span.source() {
                self.span.start()
            } else {
                labels[0].char_span.start
            };
            let line_and_col = match self.config.index_type {
                IndexType::Char => src.get_offset_line(location),
                IndexType::Byte => src.get_byte_line(location).map(|(line_obj, idx, col)| {
                    let line_text = src.get_line_text(line_obj).unwrap();

                    let col = line_text[..col.min(line_text.len())].chars().count();

                    (line_obj, idx, col)
                }),
            };
            let (line_no, col_no) = line_and_col
                .map(|(_, idx, col)| {
                    (
                        format!("{}", idx + 1 + src.display_line_offset()),
                        format!("{}", col + 1),
                    )
                })
                .unwrap_or_else(|| ('?'.to_string(), '?'.to_string()));
            let line_ref = format!("{}:{}:{}", src_name, line_no, col_no);
            writeln!(
                w,
                "{}{}{}{} {} {}",
                Show((' ', line_no_width + 2)),
                if group_idx == 0 {
                    draw.ltop
                } else {
                    draw.lcross
                }
                .fg(self.config.margin_color(), s),
                draw.hbar.fg(self.config.margin_color(), s),
                draw.lbox.fg(self.config.margin_color(), s),
                line_ref,
                draw.rbox.fg(self.config.margin_color(), s),
            )?;

            if !self.config.compact {
                writeln!(
                    w,
                    "{}{}",
                    Show((' ', line_no_width + 2)),
                    draw.vbar.fg(self.config.margin_color(), s)
                )?;
            }

            struct LineLabel<'a> {
                col: usize,
                label: &'a LabelInfo<'a>,
                multi: bool,
                draw_msg: bool,
            }

            // Generate a list of multi-line labels
            let mut multi_labels = Vec::new();
            let mut multi_labels_with_message = Vec::new();
            for label_info in &labels {
                if matches!(label_info.kind, LabelKind::Multiline) {
                    multi_labels.push(label_info);
                    if label_info.display_info.msg.is_some() {
                        multi_labels_with_message.push(label_info);
                    }
                }
            }

            // Sort multiline labels by length
            multi_labels.sort_by_key(|m| -(Span::len(&m.char_span) as isize));
            multi_labels_with_message.sort_by_key(|m| -(Span::len(&m.char_span) as isize));

            let write_margin = |w: &mut W,
                                idx: usize,
                                is_line: bool,
                                is_ellipsis: bool,
                                draw_labels: bool,
                                report_row: Option<(usize, bool)>,
                                line_labels: &[LineLabel],
                                margin_label: &Option<LineLabel>|
             -> std::io::Result<()> {
                let line_no_margin = if is_line && !is_ellipsis {
                    let line_no = format!("{}", idx + 1);
                    format!(
                        "{}{} {}",
                        Show((' ', line_no_width - line_no.chars().count())),
                        line_no,
                        draw.vbar,
                    )
                    .fg(self.config.margin_color(), s)
                } else {
                    format!(
                        "{}{}",
                        Show((' ', line_no_width + 1)),
                        if is_ellipsis {
                            draw.vbar_gap
                        } else {
                            draw.vbar
                        }
                    )
                    .fg(self.config.skipped_margin_color(), s)
                };

                write!(
                    w,
                    " {}{}",
                    line_no_margin,
                    Show(Some(' ').filter(|_| !self.config.compact)),
                )?;

                // Multi-line margins
                if draw_labels {
                    for col in 0..multi_labels_with_message.len()
                        + (!multi_labels_with_message.is_empty()) as usize
                    {
                        let mut corner = None;
                        let mut hbar: Option<&LabelInfo> = None;
                        let mut vbar: Option<&LabelInfo> = None;
                        let mut margin_ptr = None;

                        let multi_label = multi_labels_with_message.get(col);
                        let line_span = src.line(idx).unwrap().span();

                        for (i, label) in multi_labels_with_message
                            [0..(col + 1).min(multi_labels_with_message.len())]
                            .iter()
                            .enumerate()
                        {
                            let margin = margin_label
                                .as_ref()
                                .filter(|m| std::ptr::eq(*label, m.label));

                            if label.char_span.start <= line_span.end
                                && label.char_span.end > line_span.start
                            {
                                let is_parent = i != col;
                                let is_start = line_span.contains(&label.char_span.start);
                                let is_end = line_span.contains(&label.last_offset());

                                if let Some(margin) = margin.filter(|_| is_line) {
                                    margin_ptr = Some((margin, is_start));
                                } else if !is_start && (!is_end || is_line) {
                                    vbar = vbar.or(Some(*label).filter(|_| !is_parent));
                                } else if let Some((report_row, is_arrow)) = report_row {
                                    let label_row = line_labels
                                        .iter()
                                        .enumerate()
                                        .find(|(_, l)| std::ptr::eq(*label, l.label))
                                        .map_or(0, |(r, _)| r);
                                    if report_row == label_row {
                                        if let Some(margin) = margin {
                                            vbar = Some(margin.label).filter(|_| col == i);
                                            if is_start {
                                                continue;
                                            }
                                        }

                                        if is_arrow {
                                            hbar = Some(*label);
                                            if !is_parent {
                                                corner = Some((label, is_start));
                                            }
                                        } else if !is_start {
                                            vbar = vbar.or(Some(*label).filter(|_| !is_parent));
                                        }
                                    } else {
                                        vbar = vbar.or(Some(*label).filter(|_| {
                                            !is_parent && (is_start ^ (report_row < label_row))
                                        }));
                                    }
                                }
                            }
                        }

                        if let (Some((margin, _is_start)), true) = (margin_ptr, is_line) {
                            let is_col =
                                multi_label.map_or(false, |ml| std::ptr::eq(*ml, margin.label));
                            let is_limit = col + 1 == multi_labels_with_message.len();
                            if !is_col && !is_limit {
                                hbar = hbar.or(Some(margin.label));
                            }
                        }

                        hbar = hbar.filter(|l| {
                            margin_label
                                .as_ref()
                                .map_or(true, |margin| !std::ptr::eq(margin.label, *l))
                                || !is_line
                        });

                        let (a, b) = if let Some((label, is_start)) = corner {
                            (
                                if is_start { draw.ltop } else { draw.lbot }
                                    .fg(label.display_info.color, s),
                                draw.hbar.fg(label.display_info.color, s),
                            )
                        } else if let Some(label) =
                            hbar.filter(|_| vbar.is_some() && !self.config.cross_gap)
                        {
                            (
                                draw.xbar.fg(label.display_info.color, s),
                                draw.hbar.fg(label.display_info.color, s),
                            )
                        } else if let Some(label) = hbar {
                            (
                                draw.hbar.fg(label.display_info.color, s),
                                draw.hbar.fg(label.display_info.color, s),
                            )
                        } else if let Some(label) = vbar {
                            (
                                if is_ellipsis {
                                    draw.vbar_gap
                                } else {
                                    draw.vbar
                                }
                                .fg(label.display_info.color, s),
                                ' '.fg(None, s),
                            )
                        } else if let (Some((margin, is_start)), true) = (margin_ptr, is_line) {
                            let is_col =
                                multi_label.map_or(false, |ml| std::ptr::eq(*ml, margin.label));
                            let is_limit = col == multi_labels_with_message.len();
                            (
                                if is_limit {
                                    draw.rarrow
                                } else if is_col {
                                    if is_start {
                                        draw.ltop
                                    } else {
                                        draw.lcross
                                    }
                                } else {
                                    draw.hbar
                                }
                                .fg(margin.label.display_info.color, s),
                                if !is_limit { draw.hbar } else { ' ' }
                                    .fg(margin.label.display_info.color, s),
                            )
                        } else {
                            (' '.fg(None, s), ' '.fg(None, s))
                        };
                        write!(w, "{}", a)?;
                        if !self.config.compact {
                            write!(w, "{}", b)?;
                        }
                    }
                }

                Ok(())
            };

            let mut is_ellipsis = false;
            for idx in line_range {
                let line = if let Some(line) = src.line(idx) {
                    line
                } else {
                    continue;
                };

                let margin_label = multi_labels_with_message
                    .iter()
                    .enumerate()
                    .filter_map(|(_i, label)| {
                        let is_start = line.span().contains(&label.char_span.start);
                        let is_end = line.span().contains(&label.last_offset());
                        if is_start {
                            // TODO: Check to see whether multi is the first on the start line or first on the end line
                            Some(LineLabel {
                                col: label.char_span.start - line.offset(),
                                label,
                                multi: true,
                                draw_msg: false, // Multi-line spans don;t have their messages drawn at the start
                            })
                        } else if is_end {
                            Some(LineLabel {
                                col: label.last_offset() - line.offset(),
                                label,
                                multi: true,
                                draw_msg: true, // Multi-line spans have their messages drawn at the end
                            })
                        } else {
                            None
                        }
                    })
                    .min_by_key(|ll| (ll.col, !ll.label.char_span.start));

                // Generate a list of labels for this line, along with their label columns
                let mut line_labels = multi_labels_with_message
                    .iter()
                    .enumerate()
                    .filter_map(|(_i, label)| {
                        let is_start = line.span().contains(&label.char_span.start);
                        let is_end = line.span().contains(&label.last_offset());
                        if is_start
                            && margin_label
                                .as_ref()
                                .map_or(true, |m| !std::ptr::eq(*label, m.label))
                        {
                            // TODO: Check to see whether multi is the first on the start line or first on the end line
                            Some(LineLabel {
                                col: label.char_span.start - line.offset(),
                                label,
                                multi: true,
                                draw_msg: false, // Multi-line spans don;t have their messages drawn at the start
                            })
                        } else if is_end {
                            Some(LineLabel {
                                col: label.last_offset() - line.offset(),
                                label,
                                multi: true,
                                draw_msg: true, // Multi-line spans have their messages drawn at the end
                            })
                        } else {
                            None
                        }
                    })
                    .collect::<Vec<_>>();

                for label_info in labels.iter().filter(|l| {
                    l.char_span.start >= line.span().start && l.char_span.end <= line.span().end
                }) {
                    if matches!(label_info.kind, LabelKind::Inline) {
                        line_labels.push(LineLabel {
                            col: match &self.config.label_attach {
                                LabelAttach::Start => label_info.char_span.start,
                                LabelAttach::Middle => {
                                    (label_info.char_span.start + label_info.char_span.end) / 2
                                }
                                LabelAttach::End => label_info.last_offset(),
                            }
                            .max(label_info.char_span.start)
                                - line.offset(),
                            label: label_info,
                            multi: false,
                            draw_msg: true,
                        });
                    }
                }

                // Skip this line if we don't have labels for it
                if line_labels.is_empty() && margin_label.is_none() {
                    let within_label = multi_labels
                        .iter()
                        .any(|label| label.char_span.contains(&line.span().start()));
                    if !is_ellipsis && within_label {
                        is_ellipsis = true;
                    } else {
                        if !self.config.compact && !is_ellipsis {
                            write_margin(&mut w, idx, false, is_ellipsis, false, None, &[], &None)?;
                            writeln!(w)?;
                        }
                        is_ellipsis = true;
                        continue;
                    }
                } else {
                    is_ellipsis = false;
                }

                // Sort the labels by their columns
                line_labels.sort_by_key(|ll| {
                    (
                        ll.label.display_info.order,
                        ll.col,
                        !ll.label.char_span.start,
                    )
                });

                // Determine label bounds so we know where to put error messages
                let arrow_end_space = if self.config.compact { 1 } else { 2 };
                let arrow_len = line_labels.iter().fold(0, |l, ll| {
                    if ll.multi {
                        line.len()
                    } else {
                        l.max(ll.label.char_span.end().saturating_sub(line.offset()))
                    }
                }) + arrow_end_space;

                // Should we draw a vertical bar as part of a label arrow on this line?
                let get_vbar = |col, row| {
                    line_labels
                        .iter()
                        // Only labels with notes get an arrow
                        .enumerate()
                        .filter(|(_, ll)| {
                            ll.label.display_info.msg.is_some()
                                && margin_label
                                    .as_ref()
                                    .map_or(true, |m| !std::ptr::eq(ll.label, m.label))
                        })
                        .find(|(j, ll)| ll.col == col && row <= *j)
                        .map(|(_, ll)| ll)
                };

                let get_highlight = |col| {
                    margin_label
                        .iter()
                        .map(|ll| &ll.label)
                        .chain(multi_labels.iter())
                        .chain(line_labels.iter().map(|l| &l.label))
                        .filter(|l| l.char_span.contains(&(line.offset() + col)))
                        // Prioritise displaying smaller spans
                        .min_by_key(|l| {
                            (
                                -l.display_info.priority,
                                ExactSizeIterator::len(&l.char_span),
                            )
                        })
                };

                let get_underline = |col| {
                    line_labels
                        .iter()
                        .filter(|ll| {
                            self.config.underlines
                        // Underlines only occur for inline spans (highlighting can occur for all spans)
                        && !ll.multi
                        && ll.label.char_span.contains(&(line.offset() + col))
                        })
                        // Prioritise displaying smaller spans
                        .min_by_key(|ll| {
                            (
                                -ll.label.display_info.priority,
                                ExactSizeIterator::len(&ll.label.char_span),
                            )
                        })
                };

                // Margin
                write_margin(
                    &mut w,
                    idx,
                    true,
                    is_ellipsis,
                    true,
                    None,
                    &line_labels,
                    &margin_label,
                )?;

                // Line
                if !is_ellipsis {
                    for (col, c) in src
                        .get_line_text(line)
                        .unwrap()
                        .trim_end()
                        .chars()
                        .enumerate()
                    {
                        let color = if let Some(highlight) = get_highlight(col) {
                            highlight.display_info.color
                        } else {
                            self.config.unimportant_color()
                        };
                        let (c, width) = self.config.char_width(c, col);
                        if c.is_whitespace() {
                            for _ in 0..width {
                                write!(w, "{}", c.fg(color, s))?;
                            }
                        } else {
                            write!(w, "{}", c.fg(color, s))?;
                        };
                    }
                }
                writeln!(w)?;

                // Arrows
                for row in 0..line_labels.len() {
                    let line_label = &line_labels[row];
                    //No message to draw thus no arrow to draw
                    if line_label.label.display_info.msg.is_none() {
                        continue;
                    }
                    if !self.config.compact {
                        // Margin alternate
                        write_margin(
                            &mut w,
                            idx,
                            false,
                            is_ellipsis,
                            true,
                            Some((row, false)),
                            &line_labels,
                            &margin_label,
                        )?;
                        // Lines alternate
                        let mut chars = src.get_line_text(line).unwrap().trim_end().chars();
                        for col in 0..arrow_len {
                            let width =
                                chars.next().map_or(1, |c| self.config.char_width(c, col).1);

                            let vbar = get_vbar(col, row);
                            let underline = get_underline(col).filter(|_| row == 0);
                            let [c, tail] = if let Some(vbar_ll) = vbar {
                                let [c, tail] = if underline.is_some() {
                                    // TODO: Is this good?
                                    // The `true` is used here because it's temporarily disabling a
                                    // feature that might be reenabled later.
                                    #[allow(clippy::overly_complex_bool_expr)]
                                    if ExactSizeIterator::len(&vbar_ll.label.char_span) <= 1 || true
                                    {
                                        [draw.underbar, draw.underline]
                                    } else if line.offset() + col == vbar_ll.label.char_span.start {
                                        [draw.ltop, draw.underbar]
                                    } else if line.offset() + col == vbar_ll.label.last_offset() {
                                        [draw.rtop, draw.underbar]
                                    } else {
                                        [draw.underbar, draw.underline]
                                    }
                                } else if vbar_ll.multi && row == 0 && self.config.multiline_arrows
                                {
                                    [draw.uarrow, ' ']
                                } else {
                                    [draw.vbar, ' ']
                                };
                                [
                                    c.fg(vbar_ll.label.display_info.color, s),
                                    tail.fg(vbar_ll.label.display_info.color, s),
                                ]
                            } else if let Some(underline_ll) = underline {
                                [draw.underline.fg(underline_ll.label.display_info.color, s); 2]
                            } else {
                                [' '.fg(None, s); 2]
                            };

                            for i in 0..width {
                                write!(w, "{}", if i == 0 { c } else { tail })?;
                            }
                        }
                        writeln!(w)?;
                    }

                    // Margin
                    write_margin(
                        &mut w,
                        idx,
                        false,
                        is_ellipsis,
                        true,
                        Some((row, true)),
                        &line_labels,
                        &margin_label,
                    )?;
                    // Lines
                    let mut chars = src.get_line_text(line).unwrap().trim_end().chars();
                    for col in 0..arrow_len {
                        let width = chars.next().map_or(1, |c| self.config.char_width(c, col).1);

                        let is_hbar = (((col > line_label.col) ^ line_label.multi)
                            || (line_label.label.display_info.msg.is_some()
                                && line_label.draw_msg
                                && col > line_label.col))
                            && line_label.label.display_info.msg.is_some();
                        let [c, tail] = if col == line_label.col
                            && line_label.label.display_info.msg.is_some()
                            && margin_label
                                .as_ref()
                                .map_or(true, |m| !std::ptr::eq(line_label.label, m.label))
                        {
                            [
                                if line_label.multi {
                                    if line_label.draw_msg {
                                        draw.mbot
                                    } else {
                                        draw.rbot
                                    }
                                } else {
                                    draw.lbot
                                }
                                .fg(line_label.label.display_info.color, s),
                                draw.hbar.fg(line_label.label.display_info.color, s),
                            ]
                        } else if let Some(vbar_ll) = get_vbar(col, row).filter(|_| {
                            col != line_label.col || line_label.label.display_info.msg.is_some()
                        }) {
                            if !self.config.cross_gap && is_hbar {
                                [
                                    draw.xbar.fg(line_label.label.display_info.color, s),
                                    ' '.fg(line_label.label.display_info.color, s),
                                ]
                            } else if is_hbar {
                                [draw.hbar.fg(line_label.label.display_info.color, s); 2]
                            } else {
                                [
                                    if vbar_ll.multi && row == 0 && self.config.compact {
                                        draw.uarrow
                                    } else {
                                        draw.vbar
                                    }
                                    .fg(vbar_ll.label.display_info.color, s),
                                    ' '.fg(line_label.label.display_info.color, s),
                                ]
                            }
                        } else if is_hbar {
                            [draw.hbar.fg(line_label.label.display_info.color, s); 2]
                        } else {
                            [' '.fg(None, s); 2]
                        };

                        if width > 0 {
                            write!(w, "{}", c)?;
                        }
                        for _ in 1..width {
                            write!(w, "{}", tail)?;
                        }
                    }
                    if line_label.draw_msg {
                        write!(w, " {}", Show(line_label.label.display_info.msg.as_ref()))?;
                    }
                    writeln!(w)?;
                }
            }

            let is_final_group = group_idx + 1 == groups_len;

            // Help
            if is_final_group {
                for (i, help) in self.help.iter().enumerate() {
                    if !self.config.compact {
                        write_margin(&mut w, 0, false, false, true, Some((0, false)), &[], &None)?;
                        writeln!(w)?;
                    }
                    let help_prefix = format!("{} {}", "Help", i + 1);
                    let help_prefix_len = if self.help.len() > 1 {
                        help_prefix.len()
                    } else {
                        4
                    };
                    let mut lines = help.lines();
                    if let Some(line) = lines.next() {
                        write_margin(&mut w, 0, false, false, true, Some((0, false)), &[], &None)?;
                        if self.help.len() > 1 {
                            writeln!(
                                w,
                                "{}: {}",
                                help_prefix.fg(self.config.note_color(), s),
                                line
                            )?;
                        } else {
                            writeln!(w, "{}: {}", "Help".fg(self.config.note_color(), s), line)?;
                        }
                    }
                    for line in lines {
                        write_margin(&mut w, 0, false, false, true, Some((0, false)), &[], &None)?;
                        writeln!(w, "{:>pad$}{}", "", line, pad = help_prefix_len + 2)?;
                    }
                }
            }

            // Note
            if is_final_group {
                for (i, note) in self.notes.iter().enumerate() {
                    if !self.config.compact {
                        write_margin(&mut w, 0, false, false, true, Some((0, false)), &[], &None)?;
                        writeln!(w)?;
                    }
                    let note_prefix = format!("{} {}", "Note", i + 1);
                    let note_prefix_len = if self.notes.len() > 1 {
                        note_prefix.len()
                    } else {
                        4
                    };
                    let mut lines = note.lines();
                    if let Some(line) = lines.next() {
                        write_margin(&mut w, 0, false, false, true, Some((0, false)), &[], &None)?;
                        if self.notes.len() > 1 {
                            writeln!(
                                w,
                                "{}: {}",
                                note_prefix.fg(self.config.note_color(), s),
                                line
                            )?;
                        } else {
                            writeln!(w, "{}: {}", "Note".fg(self.config.note_color(), s), line)?;
                        }
                    }
                    for line in lines {
                        write_margin(&mut w, 0, false, false, true, Some((0, false)), &[], &None)?;
                        writeln!(w, "{:>pad$}{}", "", line, pad = note_prefix_len + 2)?;
                    }
                }
            }

            // Tail of report
            if !self.config.compact {
                if is_final_group {
                    let final_margin =
                        format!("{}{}", Show((draw.hbar, line_no_width + 2)), draw.rbot);
                    writeln!(w, "{}", final_margin.fg(self.config.margin_color(), s))?;
                } else {
                    writeln!(
                        w,
                        "{}{}",
                        Show((' ', line_no_width + 2)),
                        draw.vbar.fg(self.config.margin_color(), s)
                    )?;
                }
            }
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    //! These tests use [insta](https://insta.rs/). If you do `cargo install cargo-insta` you can
    //! automatically update the snapshots with `cargo insta review` or `cargo insta accept`.
    //!
    //! When adding new tests you can leave the string in the `assert_snapshot!` macro call empty:
    //!
    //!     assert_snapshot!(msg, @"");
    //!
    //! and insta will fill it in.

    use insta::assert_snapshot;

    use crate::{Cache, CharSet, Config, IndexType, Label, Report, ReportKind, Source, Span};

    impl<S: Span> Report<'_, S> {
        fn write_to_string<C: Cache<S::SourceId>>(&self, cache: C) -> String {
            let mut vec = Vec::new();
            self.write(cache, &mut vec).unwrap();
            String::from_utf8(vec).unwrap()
        }
    }

    fn no_color_and_ascii() -> Config {
        Config::default()
            .with_color(false)
            // Using Ascii so that the inline snapshots display correctly
            // even with fonts where characters like '┬' take up more space.
            .with_char_set(CharSet::Ascii)
    }

    fn remove_trailing(s: String) -> String {
        s.lines().flat_map(|l| [l.trim_end(), "\n"]).collect()
    }

    #[test]
    fn one_message() {
        let msg = remove_trailing(
            Report::build(ReportKind::Error, 0..0)
                .with_config(no_color_and_ascii())
                .with_message("can't compare apples with oranges")
                .finish()
                .write_to_string(Source::from("")),
        );
        assert_snapshot!(msg, @r###"
        Error: can't compare apples with oranges
        "###)
    }

    #[test]
    fn two_labels_without_messages() {
        let source = "apple == orange;";
        let msg = remove_trailing(
            Report::build(ReportKind::Error, 0..0)
                .with_config(no_color_and_ascii())
                .with_message("can't compare apples with oranges")
                .with_label(Label::new(0..5))
                .with_label(Label::new(9..15))
                .finish()
                .write_to_string(Source::from(source)),
        );
        // TODO: it would be nice if these spans still showed up (like codespan-reporting does)
        assert_snapshot!(msg, @r###"
        Error: can't compare apples with oranges
           ,-[ <unknown>:1:1 ]
           |
         1 | apple == orange;
        ---'
        "###);
    }

    #[test]
    fn two_labels_with_messages() {
        let source = "apple == orange;";
        let msg = remove_trailing(
            Report::build(ReportKind::Error, 0..0)
                .with_config(no_color_and_ascii())
                .with_message("can't compare apples with oranges")
                .with_label(Label::new(0..5).with_message("This is an apple"))
                .with_label(Label::new(9..15).with_message("This is an orange"))
                .finish()
                .write_to_string(Source::from(source)),
        );
        // TODO: it would be nice if these lines didn't cross
        assert_snapshot!(msg, @r###"
        Error: can't compare apples with oranges
           ,-[ <unknown>:1:1 ]
           |
         1 | apple == orange;
           | ^^|^^    ^^^|^^
           |   `-------------- This is an apple
           |             |
           |             `---- This is an orange
        ---'
        "###);
    }

    #[test]
    fn multi_byte_chars() {
        let source = "äpplë == örängë;";
        let msg = remove_trailing(
            Report::build(ReportKind::Error, 0..0)
                .with_config(no_color_and_ascii().with_index_type(IndexType::Char))
                .with_message("can't compare äpplës with örängës")
                .with_label(Label::new(0..5).with_message("This is an äpplë"))
                .with_label(Label::new(9..15).with_message("This is an örängë"))
                .finish()
                .write_to_string(Source::from(source)),
        );
        // TODO: it would be nice if these lines didn't cross
        assert_snapshot!(msg, @r###"
        Error: can't compare äpplës with örängës
           ,-[ <unknown>:1:1 ]
           |
         1 | äpplë == örängë;
           | ^^|^^    ^^^|^^
           |   `-------------- This is an äpplë
           |             |
           |             `---- This is an örängë
        ---'
        "###);
    }

    #[test]
    fn byte_label() {
        let source = "äpplë == örängë;";
        let msg = remove_trailing(
            Report::build(ReportKind::Error, 0..0)
                .with_config(no_color_and_ascii().with_index_type(IndexType::Byte))
                .with_message("can't compare äpplës with örängës")
                .with_label(Label::new(0..7).with_message("This is an äpplë"))
                .with_label(Label::new(11..20).with_message("This is an örängë"))
                .finish()
                .write_to_string(Source::from(source)),
        );
        // TODO: it would be nice if these lines didn't cross
        assert_snapshot!(msg, @r###"
        Error: can't compare äpplës with örängës
           ,-[ <unknown>:1:1 ]
           |
         1 | äpplë == örängë;
           | ^^|^^    ^^^|^^
           |   `-------------- This is an äpplë
           |             |
           |             `---- This is an örängë
        ---'
        "###);
    }

    #[test]
    fn byte_column() {
        let source = "äpplë == örängë;";
        let msg = remove_trailing(
            Report::build(ReportKind::Error, 11..11)
                .with_config(no_color_and_ascii().with_index_type(IndexType::Byte))
                .with_message("can't compare äpplës with örängës")
                .with_label(Label::new(0..7).with_message("This is an äpplë"))
                .with_label(Label::new(11..20).with_message("This is an örängë"))
                .finish()
                .write_to_string(Source::from(source)),
        );
        // TODO: it would be nice if these lines didn't cross
        assert_snapshot!(msg, @r###"
        Error: can't compare äpplës with örängës
           ,-[ <unknown>:1:10 ]
           |
         1 | äpplë == örängë;
           | ^^|^^    ^^^|^^
           |   `-------------- This is an äpplë
           |             |
           |             `---- This is an örängë
        ---'
        "###);
    }

    #[test]
    fn label_at_end_of_long_line() {
        let source = format!("{}orange", "apple == ".repeat(100));
        let msg = remove_trailing(
            Report::build(ReportKind::Error, 0..0)
                .with_config(no_color_and_ascii())
                .with_message("can't compare apples with oranges")
                .with_label(
                    Label::new(source.len() - 5..source.len()).with_message("This is an orange"),
                )
                .finish()
                .write_to_string(Source::from(source)),
        );
        // TODO: it would be nice if the start of long lines would be omitted (like rustc does)
        assert_snapshot!(msg, @r###"
        Error: can't compare apples with oranges
           ,-[ <unknown>:1:1 ]
           |
         1 | apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == apple == orange
           |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      ^^|^^
           |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        `---- This is an orange
        ---'
        "###);
    }

    #[test]
    fn label_of_width_zero_at_end_of_line() {
        let source = "apple ==\n";
        let msg = remove_trailing(
            Report::build(ReportKind::Error, 0..0)
                .with_config(no_color_and_ascii().with_index_type(IndexType::Byte))
                .with_message("unexpected end of file")
                .with_label(Label::new(9..9).with_message("Unexpected end of file"))
                .finish()
                .write_to_string(Source::from(source)),
        );

        assert_snapshot!(msg, @r###"
        Error: unexpected end of file
           ,-[ <unknown>:1:1 ]
           |
         1 | apple ==
           |          |
           |          `- Unexpected end of file
        ---'
        "###);
    }

    #[test]
    fn empty_input() {
        let source = "";
        let msg = remove_trailing(
            Report::build(ReportKind::Error, 0..0)
                .with_config(no_color_and_ascii())
                .with_message("unexpected end of file")
                .with_label(Label::new(0..0).with_message("No more fruit!"))
                .finish()
                .write_to_string(Source::from(source)),
        );

        assert_snapshot!(msg, @r###"
        Error: unexpected end of file
           ,-[ <unknown>:1:1 ]
           |
         1 |
           | |
           | `- No more fruit!
        ---'
        "###);
    }

    #[test]
    fn empty_input_help() {
        let source = "";
        let msg = remove_trailing(
            Report::build(ReportKind::Error, 0..0)
                .with_config(no_color_and_ascii())
                .with_message("unexpected end of file")
                .with_label(Label::new(0..0).with_message("No more fruit!"))
                .with_help("have you tried going to the farmer's market?")
                .finish()
                .write_to_string(Source::from(source)),
        );

        assert_snapshot!(msg, @r###"
        Error: unexpected end of file
           ,-[ <unknown>:1:1 ]
           |
         1 |
           | |
           | `- No more fruit!
           |
           | Help: have you tried going to the farmer's market?
        ---'
        "###);
    }

    #[test]
    fn empty_input_note() {
        let source = "";
        let msg = remove_trailing(
            Report::build(ReportKind::Error, 0..0)
                .with_config(no_color_and_ascii())
                .with_message("unexpected end of file")
                .with_label(Label::new(0..0).with_message("No more fruit!"))
                .with_note("eat your greens!")
                .finish()
                .write_to_string(Source::from(source)),
        );

        assert_snapshot!(msg, @r###"
        Error: unexpected end of file
           ,-[ <unknown>:1:1 ]
           |
         1 |
           | |
           | `- No more fruit!
           |
           | Note: eat your greens!
        ---'
        "###);
    }

    #[test]
    fn empty_input_help_note() {
        let source = "";
        let msg = remove_trailing(
            Report::build(ReportKind::Error, 0..0)
                .with_config(no_color_and_ascii())
                .with_message("unexpected end of file")
                .with_label(Label::new(0..0).with_message("No more fruit!"))
                .with_note("eat your greens!")
                .with_help("have you tried going to the farmer's market?")
                .finish()
                .write_to_string(Source::from(source)),
        );

        assert_snapshot!(msg, @r###"
        Error: unexpected end of file
           ,-[ <unknown>:1:1 ]
           |
         1 |
           | |
           | `- No more fruit!
           |
           | Help: have you tried going to the farmer's market?
           |
           | Note: eat your greens!
        ---'
        "###);
    }

    #[test]
    fn byte_spans_never_crash() {
        let source = "apple\np\n\nempty\n";

        for i in 0..=source.len() {
            for j in i..=source.len() {
                let _ = remove_trailing(
                    Report::build(ReportKind::Error, 0..0)
                        .with_config(no_color_and_ascii().with_index_type(IndexType::Byte))
                        .with_message("Label")
                        .with_label(Label::new(i..j).with_message("Label"))
                        .finish()
                        .write_to_string(Source::from(source)),
                );
            }
        }
    }

    #[test]
    fn multiline_label() {
        let source = "apple\n==\norange";
        let msg = remove_trailing(
            Report::build(ReportKind::Error, 0..0)
                .with_config(no_color_and_ascii())
                .with_label(Label::new(0..source.len()).with_message("illegal comparison"))
                .finish()
                .write_to_string(Source::from(source)),
        );
        // TODO: it would be nice if the 2nd line wasn't omitted
        assert_snapshot!(msg, @r###"
        Error:
           ,-[ <unknown>:1:1 ]
           |
         1 | ,-> apple
           : :
         3 | |-> orange
           | |
           | `----------- illegal comparison
        ---'
        "###);
    }

    #[test]
    fn partially_overlapping_labels() {
        let source = "https://example.com/";
        let msg = remove_trailing(
            Report::build(ReportKind::Error, 0..0)
                .with_config(no_color_and_ascii())
                .with_label(Label::new(0..source.len()).with_message("URL"))
                .with_label(Label::new(0..source.find(':').unwrap()).with_message("scheme"))
                .finish()
                .write_to_string(Source::from(source)),
        );
        // TODO: it would be nice if you could tell where the spans start and end.
        assert_snapshot!(msg, @r###"
        Error:
           ,-[ <unknown>:1:1 ]
           |
         1 | https://example.com/
           | ^^|^^^^^^^|^^^^^^^^^
           |   `------------------- scheme
           |           |
           |           `----------- URL
        ---'
        "###);
    }

    #[test]
    fn multiple_labels_same_span() {
        let source = "apple == orange;";
        let msg = remove_trailing(
            Report::build(ReportKind::Error, 0..0)
                .with_config(no_color_and_ascii())
                .with_message("can't compare apples with oranges")
                .with_label(Label::new(0..5).with_message("This is an apple"))
                .with_label(
                    Label::new(0..5).with_message("Have I mentioned that this is an apple?"),
                )
                .with_label(Label::new(0..5).with_message("No really, have I mentioned that?"))
                .with_label(Label::new(9..15).with_message("This is an orange"))
                .with_label(
                    Label::new(9..15).with_message("Have I mentioned that this is an orange?"),
                )
                .with_label(Label::new(9..15).with_message("No really, have I mentioned that?"))
                .finish()
                .write_to_string(Source::from(source)),
        );
        assert_snapshot!(msg, @r###"
        Error: can't compare apples with oranges
           ,-[ <unknown>:1:1 ]
           |
         1 | apple == orange;
           | ^^|^^    ^^^|^^
           |   `-------------- This is an apple
           |   |         |
           |   `-------------- Have I mentioned that this is an apple?
           |   |         |
           |   `-------------- No really, have I mentioned that?
           |             |
           |             `---- This is an orange
           |             |
           |             `---- Have I mentioned that this is an orange?
           |             |
           |             `---- No really, have I mentioned that?
        ---'
        "###)
    }

    #[test]
    fn note() {
        let source = "apple == orange;";
        let msg = remove_trailing(
            Report::build(ReportKind::Error, 0..0)
                .with_config(no_color_and_ascii())
                .with_message("can't compare apples with oranges")
                .with_label(Label::new(0..5).with_message("This is an apple"))
                .with_label(Label::new(9..15).with_message("This is an orange"))
                .with_note("stop trying ... this is a fruitless endeavor")
                .finish()
                .write_to_string(Source::from(source)),
        );
        assert_snapshot!(msg, @r###"
        Error: can't compare apples with oranges
           ,-[ <unknown>:1:1 ]
           |
         1 | apple == orange;
           | ^^|^^    ^^^|^^
           |   `-------------- This is an apple
           |             |
           |             `---- This is an orange
           |
           | Note: stop trying ... this is a fruitless endeavor
        ---'
        "###)
    }

    #[test]
    fn help() {
        let source = "apple == orange;";
        let msg = remove_trailing(
            Report::build(ReportKind::Error, 0..0)
                .with_config(no_color_and_ascii())
                .with_message("can't compare apples with oranges")
                .with_label(Label::new(0..5).with_message("This is an apple"))
                .with_label(Label::new(9..15).with_message("This is an orange"))
                .with_help("have you tried peeling the orange?")
                .finish()
                .write_to_string(Source::from(source)),
        );
        assert_snapshot!(msg, @r###"
        Error: can't compare apples with oranges
           ,-[ <unknown>:1:1 ]
           |
         1 | apple == orange;
           | ^^|^^    ^^^|^^
           |   `-------------- This is an apple
           |             |
           |             `---- This is an orange
           |
           | Help: have you tried peeling the orange?
        ---'
        "###)
    }

    #[test]
    fn help_and_note() {
        let source = "apple == orange;";
        let msg = remove_trailing(
            Report::build(ReportKind::Error, 0..0)
                .with_config(no_color_and_ascii())
                .with_message("can't compare apples with oranges")
                .with_label(Label::new(0..5).with_message("This is an apple"))
                .with_label(Label::new(9..15).with_message("This is an orange"))
                .with_help("have you tried peeling the orange?")
                .with_note("stop trying ... this is a fruitless endeavor")
                .finish()
                .write_to_string(Source::from(source)),
        );
        assert_snapshot!(msg, @r###"
        Error: can't compare apples with oranges
           ,-[ <unknown>:1:1 ]
           |
         1 | apple == orange;
           | ^^|^^    ^^^|^^
           |   `-------------- This is an apple
           |             |
           |             `---- This is an orange
           |
           | Help: have you tried peeling the orange?
           |
           | Note: stop trying ... this is a fruitless endeavor
        ---'
        "###)
    }

    #[test]
    fn single_note_single_line() {
        let source = "apple == orange;";
        let msg = remove_trailing(
            Report::build(ReportKind::Error, 0..0)
                .with_config(no_color_and_ascii())
                .with_message("can't compare apples with oranges")
                .with_label(Label::new(0..15).with_message("This is a strange comparison"))
                .with_note("No need to try, they can't be compared.")
                .finish()
                .write_to_string(Source::from(source)),
        );
        assert_snapshot!(msg, @r###"
        Error: can't compare apples with oranges
           ,-[ <unknown>:1:1 ]
           |
         1 | apple == orange;
           | ^^^^^^^|^^^^^^^
           |        `--------- This is a strange comparison
           |
           | Note: No need to try, they can't be compared.
        ---'
        "###)
    }

    #[test]
    fn multi_notes_single_lines() {
        let source = "apple == orange;";
        let msg = remove_trailing(
            Report::build(ReportKind::Error, 0..0)
                .with_config(no_color_and_ascii())
                .with_message("can't compare apples with oranges")
                .with_label(Label::new(0..15).with_message("This is a strange comparison"))
                .with_note("No need to try, they can't be compared.")
                .with_note("Yeah, really, please stop.")
                .finish()
                .write_to_string(Source::from(source)),
        );
        assert_snapshot!(msg, @r###"
        Error: can't compare apples with oranges
           ,-[ <unknown>:1:1 ]
           |
         1 | apple == orange;
           | ^^^^^^^|^^^^^^^
           |        `--------- This is a strange comparison
           |
           | Note 1: No need to try, they can't be compared.
           |
           | Note 2: Yeah, really, please stop.
        ---'
        "###)
    }

    #[test]
    fn multi_notes_multi_lines() {
        let source = "apple == orange;";
        let msg = remove_trailing(
            Report::build(ReportKind::Error, 0..0)
                .with_config(no_color_and_ascii())
                .with_message("can't compare apples with oranges")
                .with_label(Label::new(0..15).with_message("This is a strange comparison"))
                .with_note("No need to try, they can't be compared.")
                .with_note("Yeah, really, please stop.\nIt has no resemblance.")
                .finish()
                .write_to_string(Source::from(source)),
        );
        assert_snapshot!(msg, @r###"
        Error: can't compare apples with oranges
           ,-[ <unknown>:1:1 ]
           |
         1 | apple == orange;
           | ^^^^^^^|^^^^^^^
           |        `--------- This is a strange comparison
           |
           | Note 1: No need to try, they can't be compared.
           |
           | Note 2: Yeah, really, please stop.
           |         It has no resemblance.
        ---'
        "###)
    }

    #[test]
    fn multi_helps_multi_lines() {
        let source = "apple == orange;";
        let msg = remove_trailing(
            Report::build(ReportKind::Error, 0..0)
                .with_config(no_color_and_ascii())
                .with_message("can't compare apples with oranges")
                .with_label(Label::new(0..15).with_message("This is a strange comparison"))
                .with_help("No need to try, they can't be compared.")
                .with_help("Yeah, really, please stop.\nIt has no resemblance.")
                .finish()
                .write_to_string(Source::from(source)),
        );
        assert_snapshot!(msg, @r###"
        Error: can't compare apples with oranges
           ,-[ <unknown>:1:1 ]
           |
         1 | apple == orange;
           | ^^^^^^^|^^^^^^^
           |        `--------- This is a strange comparison
           |
           | Help 1: No need to try, they can't be compared.
           |
           | Help 2: Yeah, really, please stop.
           |         It has no resemblance.
        ---'
        "###)
    }

    #[test]
    fn ordered_labels() {
        let msg = remove_trailing(
            Report::build(ReportKind::Error, ("", 0..0))
                .with_config(no_color_and_ascii())
                .with_label(Label::new(("b", 13..18)).with_order(1).with_message("1"))
                .with_label(Label::new(("a", 0..6)).with_order(2).with_message("2"))
                .with_label(Label::new(("a", 7..12)).with_order(3).with_message("3"))
                .with_label(Label::new(("b", 0..6)).with_order(4).with_message("4"))
                .with_label(Label::new(("b", 7..12)).with_order(5).with_message("5"))
                .finish()
                .write_to_string(crate::sources([
                    ("a", "second\nthird"),
                    ("b", "fourth\nfifth\nfirst"),
                ])),
        );
        assert_snapshot!(msg, @r###"
        Error:
           ,-[ b:3:1 ]
           |
         3 | first
           | ^^|^^
           |   `---- 1
           |
           |-[ a:1:1 ]
           |
         1 | second
           | ^^^|^^
           |    `---- 2
         2 | third
           | ^^|^^
           |   `---- 3
           |
           |-[ b:1:1 ]
           |
         1 | fourth
           | ^^^|^^
           |    `---- 4
         2 | fifth
           | ^^|^^
           |   `---- 5
        ---'
        "###)
    }
}
