use std::sync::Arc;

use typst_library::foundations::{AlternativeFold, Fold};
use typst_library::layout::grid::resolve::{CellGrid, Line, Repeatable};
use typst_library::layout::Abs;
use typst_library::visualize::Stroke;

use super::RowPiece;

/// Indicates which priority a particular grid line segment should have, based
/// on the highest priority configuration that defined the segment's stroke.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum StrokePriority {
    /// The stroke of the segment was derived solely from the grid's global
    /// stroke setting, so it should have the lowest priority.
    GridStroke = 0,
    /// The segment's stroke was derived (even if partially) from a cell's
    /// stroke override, so it should have priority over non-overridden cell
    /// strokes and be drawn on top of them (when they have the same
    /// thickness).
    CellStroke = 1,
    /// The segment's stroke was derived from a user's explicitly placed line
    /// (hline or vline), and thus should have maximum priority, drawn on top
    /// of any cell strokes (when they have the same thickness).
    ExplicitLine = 2,
}

/// Data for a particular line segment in the grid as generated by
/// `generate_line_segments`.
#[derive(Debug, PartialEq, Eq)]
pub struct LineSegment {
    /// The stroke with which to draw this segment.
    pub stroke: Arc<Stroke<Abs>>,
    /// The offset of this segment since the beginning of its axis.
    /// For a vertical line segment, this is the offset since the top of the
    /// table in the current page; for a horizontal line segment, this is the
    /// offset since the start border of the table.
    pub offset: Abs,
    /// The length of this segment.
    pub length: Abs,
    /// The segment's drawing priority, indicating on top of which other
    /// segments this one should be drawn.
    pub priority: StrokePriority,
}

/// Generates the segments of lines that should be drawn alongside a certain
/// axis in the grid, going through the given tracks (orthogonal to the lines).
/// Each returned segment contains its stroke, its offset from the start, and
/// its length.
///
/// Accepts, as parameters, the index of the lines that should be produced
/// (for example, the column at which vertical lines will be drawn); a list of
/// user-specified lines with the same index (the `lines` parameter); whether
/// the given index corresponds to the maximum index for the line's axis; and a
/// function which returns the final stroke that should be used for each track
/// the line goes through, alongside the priority of the returned stroke (its
/// parameters are the grid, the index of the line to be drawn, the number of
/// the track to draw at and the stroke of the user hline/vline override at
/// this index to fold with, if any). Contiguous segments with the same stroke
/// and priority are joined together automatically.
///
/// The function should return `None` for positions at which the line would
/// otherwise cross a merged cell (for example, a vline could cross a colspan),
/// in which case a new segment should be drawn after the merged cell(s), even
/// if it would have the same stroke as the previous one.
///
/// Regarding priority, the function should return a priority of ExplicitLine
/// when the user-defined line's stroke at the current position isn't None
/// (note that it is passed by parameter to the function). When it is None, the
/// function should return a priority of CellStroke if the stroke returned was
/// given or affected by a per-cell override of the grid's global stroke.
/// When that isn't the case, the returned stroke was entirely provided by the
/// grid's global stroke, and thus a priority of GridStroke should be returned.
///
/// Note that we assume that the tracks are sorted according to ascending
/// number, and they must be iterable over pairs of (number, size). For
/// vertical lines, for instance, `tracks` would describe the rows in the
/// current region, as pairs (row index, row height).
pub fn generate_line_segments<'grid, F, I, L>(
    grid: &'grid CellGrid,
    tracks: I,
    index: usize,
    lines: L,
    line_stroke_at_track: F,
) -> impl Iterator<Item = LineSegment> + 'grid
where
    F: Fn(
            &CellGrid,
            usize,
            usize,
            Option<Option<Arc<Stroke<Abs>>>>,
        ) -> Option<(Arc<Stroke<Abs>>, StrokePriority)>
        + 'grid,
    I: IntoIterator<Item = (usize, Abs)>,
    I::IntoIter: 'grid,
    L: IntoIterator<Item = &'grid Line>,
    L::IntoIter: Clone + 'grid,
{
    // The segment currently being drawn.
    //
    // It is extended for each consecutive track through which the line would
    // be drawn with the same stroke and priority.
    //
    // Starts as None to force us to create a new segment as soon as we find
    // the first track through which we should draw.
    let mut current_segment: Option<LineSegment> = None;

    // How far from the start (before the first track) have we gone so far.
    // Used to determine the positions at which to draw each segment.
    let mut offset = Abs::zero();

    // How much to multiply line indices by to account for gutter.
    let gutter_factor = if grid.has_gutter { 2 } else { 1 };

    // Create an iterator of line segments, which will go through each track,
    // from start to finish, to create line segments and extend them until they
    // are interrupted and thus yielded through the iterator. We then repeat
    // the process, picking up from the track after the one at which we had
    // an interruption, until we have gone through all tracks.
    //
    // When going through each track, we check if the current segment would be
    // interrupted, either because, at this track, we hit a merged cell over
    // which we shouldn't draw, or because the line would have a different
    // stroke or priority at this point (so we have to start a new segment). If
    // so, the current segment is yielded and its variable is either set to
    // 'None' (if no segment should be drawn at the point of interruption,
    // meaning we might have to create a new segment later) or to the new
    // segment (if we're starting to draw a segment with a different stroke or
    // priority than before).
    // Otherwise (if the current segment should span the current track), it is
    // simply extended (or a new one is created, if it is 'None'), and no value
    // is yielded for the current track, since the segment isn't yet complete
    // (the next tracks might extend it further before it is interrupted and
    // yielded). That is, we yield each segment only when it is interrupted,
    // since then we will know its final length for sure.
    //
    // After the loop is done (and thus we went through all tracks), we
    // interrupt the current segment one last time, to ensure the final segment
    // is always interrupted and yielded, if it wasn't interrupted earlier.
    let mut tracks = tracks.into_iter();
    let lines = lines.into_iter();
    std::iter::from_fn(move || {
        // Each time this closure runs, we advance the track iterator as much
        // as possible before returning because the current segment was
        // interrupted. The for loop is resumed from where it stopped at the
        // next call due to that, ensuring we go through all tracks and then
        // stop.
        for (track, size) in &mut tracks {
            // Get the expected line stroke at this track by folding the
            // strokes of each user-specified line (with priority to the
            // user-specified line specified last).
            let mut line_strokes = lines
                .clone()
                .filter(|line| {
                    line.end
                        .map(|end| {
                            // Subtract 1 from end index so we stop at the last
                            // cell before it (don't cross one extra gutter).
                            let end = if grid.has_gutter {
                                2 * end.get() - 1
                            } else {
                                end.get()
                            };
                            (gutter_factor * line.start..end).contains(&track)
                        })
                        .unwrap_or_else(|| track >= gutter_factor * line.start)
                })
                .map(|line| line.stroke.clone());

            // Distinguish between unspecified stroke (None, if no lines
            // were matched above) and specified stroke of None (Some(None),
            // if some lines were matched and the one specified last had a
            // stroke of None) by conditionally folding after 'next()'.
            let line_stroke = line_strokes.next().map(|first_stroke| {
                line_strokes.fold(first_stroke, |acc, line_stroke| line_stroke.fold(acc))
            });

            // The function shall determine if it is appropriate to draw
            // the line at this position or not (i.e. whether or not it
            // would cross a merged cell), and, if so, the final stroke it
            // should have (because cells near this position could have
            // stroke overrides, which have priority and should be folded
            // with the stroke obtained above).
            //
            // If we are currently already drawing a segment and the function
            // indicates we should, at this track, draw some other segment
            // (with a different stroke or priority), or even no segment at
            // all, we interrupt and yield the current segment (which was drawn
            // up to the previous track) by returning it wrapped in 'Some()'
            // (which indicates, in the context of 'std::iter::from_fn', that
            // our iterator isn't over yet, and this should be its next value).
            if let Some((stroke, priority)) =
                line_stroke_at_track(grid, index, track, line_stroke)
            {
                // We should draw at this position. Let's check if we were
                // already drawing in the previous position.
                if let Some(current_segment) = &mut current_segment {
                    // We are currently building a segment. Let's check if
                    // we should extend it to this track as well.
                    if current_segment.stroke == stroke
                        && current_segment.priority == priority
                    {
                        // Extend the current segment so it covers at least
                        // this track as well, since we should use the same
                        // stroke as in the previous one when a line goes
                        // through this track, with the same priority.
                        current_segment.length += size;
                    } else {
                        // We got a different stroke or priority now, so create
                        // a new segment with the new stroke and spanning the
                        // current track. Yield the old segment, as it was
                        // interrupted and is thus complete.
                        let new_segment =
                            LineSegment { stroke, offset, length: size, priority };
                        let old_segment = std::mem::replace(current_segment, new_segment);
                        offset += size;
                        return Some(old_segment);
                    }
                } else {
                    // We should draw here, but there is no segment
                    // currently being drawn, either because the last
                    // position had a merged cell, had a stroke
                    // of 'None', or because this is the first track.
                    // Create a new segment to draw. We start spanning this
                    // track.
                    current_segment =
                        Some(LineSegment { stroke, offset, length: size, priority });
                }
            } else if let Some(old_segment) = Option::take(&mut current_segment) {
                // We shouldn't draw here (stroke of None), so we yield the
                // current segment, as it was interrupted.
                offset += size;
                return Some(old_segment);
            }
            // Either the current segment is None (meaning we didn't start
            // drawing a segment yet since the last yielded one), so we keep
            // searching for a track where we should draw one; or the current
            // segment is Some but wasn't interrupted at this track, so we keep
            // looping through the following tracks until it is interrupted,
            // or we reach the end.
            offset += size;
        }

        // Reached the end of all tracks, so we interrupt and finish
        // the current segment. Note that, on future calls to this
        // closure, the current segment will necessarily be 'None',
        // so the iterator will necessarily end (that is, we will return None)
        // after this.
        //
        // Note: Fully-qualified notation because rust-analyzer is confused.
        Option::take(&mut current_segment)
    })
}

/// Returns the correct stroke with which to draw a vline right before column
/// `x` when going through row `y`, given the stroke of the user-specified line
/// at this position, if any (note that a stroke of `None` is unspecified,
/// while `Some(None)` means specified to remove any stroke at this position).
/// Also returns the stroke's drawing priority, which depends on its source.
///
/// If the vline would go through a colspan, returns None (shouldn't be drawn).
/// If the one (when at the border) or two (otherwise) cells to the left and
/// right of the vline have right and left stroke overrides, respectively,
/// then the cells' stroke overrides are folded together with the vline's
/// stroke (with priority to the vline's stroke, followed by the right cell's
/// stroke, and, finally, the left cell's) and returned. If only one of the two
/// cells around the vline (if there are two) has an override, that cell's
/// stroke is given priority when folding. If, however, the cells around the
/// vline at this row do not have any stroke overrides, then the vline's own
/// stroke, as defined by user-specified lines (if any), is returned.
///
/// The priority associated with the returned stroke follows the rules
/// described in the docs for `generate_line_segment`.
pub fn vline_stroke_at_row(
    grid: &CellGrid,
    x: usize,
    y: usize,
    stroke: Option<Option<Arc<Stroke<Abs>>>>,
) -> Option<(Arc<Stroke<Abs>>, StrokePriority)> {
    // When the vline isn't at the border, we need to check if a colspan would
    // be present between columns 'x' and 'x-1' at row 'y', and thus overlap
    // with the line.
    // To do so, we analyze the cell right after this vline. If it is merged
    // with a cell before this line (parent.x < x) which is at this row or
    // above it (parent.y <= y, which is checked by
    // 'effective_parent_cell_position'), this means it would overlap with the
    // vline, so the vline must not be drawn at this row.
    if x != 0 && x != grid.cols.len() {
        // Use 'effective_parent_cell_position' to skip the gutters, if x or y
        // represent gutter tracks.
        // We would then analyze the cell one column after (if at a gutter
        // column), and/or one row below (if at a gutter row), in order to
        // check if it would be merged with a cell before the vline.
        if let Some(parent) = grid.effective_parent_cell_position(x, y) {
            if parent.x < x {
                // There is a colspan cell going through this vline's position,
                // so don't draw it here.
                return None;
            }
        }
    }

    let (left_cell_stroke, left_cell_prioritized) = x
        .checked_sub(1)
        .and_then(|left_x| {
            // Let's find the parent cell of the position before us, in order
            // to take its right stroke, even with gutter before us.
            grid.effective_parent_cell_position(left_x, y)
        })
        .map(|parent| {
            let left_cell = grid.cell(parent.x, parent.y).unwrap();
            (left_cell.stroke.right.clone(), left_cell.stroke_overridden.right)
        })
        .unwrap_or((None, false));

    let (right_cell_stroke, right_cell_prioritized) = if x < grid.cols.len() {
        // Let's find the parent cell of the position after us, in order
        // to take its left stroke, even with gutter after us.
        grid.effective_parent_cell_position(x, y)
            .map(|parent| {
                let right_cell = grid.cell(parent.x, parent.y).unwrap();
                (right_cell.stroke.left.clone(), right_cell.stroke_overridden.left)
            })
            .unwrap_or((None, false))
    } else {
        (None, false)
    };

    let priority = if stroke.is_some() {
        StrokePriority::ExplicitLine
    } else if left_cell_prioritized || right_cell_prioritized {
        StrokePriority::CellStroke
    } else {
        StrokePriority::GridStroke
    };

    let (prioritized_cell_stroke, deprioritized_cell_stroke) =
        if left_cell_prioritized && !right_cell_prioritized {
            (left_cell_stroke, right_cell_stroke)
        } else {
            // When both cells' strokes have the same priority, we default to
            // prioritizing the right cell's left stroke.
            (right_cell_stroke, left_cell_stroke)
        };

    // When both cells specify a stroke for this line segment, fold
    // both strokes, with priority to either the one prioritized cell,
    // or to the right cell's left stroke in case of a tie. But when one of
    // them doesn't specify a stroke, the other cell's stroke should be used
    // instead, regardless of priority (hence the usage of 'fold_or').
    let cell_stroke = prioritized_cell_stroke.fold_or(deprioritized_cell_stroke);

    // Fold the line stroke and folded cell strokes, if possible.
    // Give priority to the explicit line stroke.
    // Otherwise, use whichever of the two isn't 'none' or unspecified.
    let final_stroke = stroke.fold_or(Some(cell_stroke)).flatten();

    final_stroke.zip(Some(priority))
}

/// Returns the correct stroke with which to draw a hline on top of row `y`
/// when going through column `x`, given the stroke of the user-specified line
/// at this position, if any (note that a stroke of `None` is unspecified,
/// while `Some(None)` means specified to remove any stroke at this position).
/// Also returns the stroke's drawing priority, which depends on its source.
///
/// The `local_top_y` parameter indicates which row is effectively on top of
/// this hline at the current region. This is `None` if the hline is above the
/// first row in the region, for instance. The `in_last_region` parameter
/// indicates whether this is the last region of the table. If not and this is
/// a line at the bottom border, the bottom border's line gains priority.
///
/// If the one (when at the border) or two (otherwise) cells above and below
/// the hline have bottom and top stroke overrides, respectively, then the
/// cells' stroke overrides are folded together with the hline's stroke (with
/// priority to hline's stroke, followed by the bottom cell's stroke, and,
/// finally, the top cell's) and returned. If only one of the two cells around
/// the vline (if there are two) has an override, that cell's stroke is given
/// priority when folding. If, however, the cells around the hline at this
/// column do not have any stroke overrides, then the hline's own stroke, as
/// defined by user-specified lines (if any), is directly returned.
///
/// The priority associated with the returned stroke follows the rules
/// described in the docs for `generate_line_segment`.
///
/// The rows argument is needed to know which rows are effectively present in
/// the current region, in order to avoid unnecessary hline splitting when a
/// rowspan's previous rows are either in a previous region or empty (and thus
/// wouldn't overlap with the hline, since its first row in the current region
/// is below the hline).
///
/// This function assumes columns are sorted by increasing `x`, and rows are
/// sorted by increasing `y`.
pub fn hline_stroke_at_column(
    grid: &CellGrid,
    rows: &[RowPiece],
    local_top_y: Option<usize>,
    in_last_region: bool,
    y: usize,
    x: usize,
    stroke: Option<Option<Arc<Stroke<Abs>>>>,
) -> Option<(Arc<Stroke<Abs>>, StrokePriority)> {
    // When the hline isn't at the border, we need to check if a rowspan
    // would be present between rows 'y' and 'y-1' at column 'x', and thus
    // overlap with the line.
    // To do so, we analyze the cell right below this hline. If it is
    // merged with a cell above this line (parent.y < y) which is at this
    // column or before it (parent.x <= x, which is checked by
    // 'effective_parent_cell_position'), this means it would overlap with the
    // hline, so the hline must not be drawn at this column.
    if y != 0 && y != grid.rows.len() {
        // Use 'effective_parent_cell_position' to skip the gutters, if x or y
        // represent gutter tracks.
        // We would then analyze the cell one column after (if at a gutter
        // column), and/or one row below (if at a gutter row), in order to
        // check if it would be merged with a cell before the hline.
        if let Some(parent) = grid.effective_parent_cell_position(x, y) {
            if parent.y < y {
                // Get the first 'y' spanned by the possible rowspan in this region.
                // The 'parent.y' row and any other spanned rows above 'y' could be
                // missing from this region, which could have lead the check above
                // to be triggered, even though there is no spanned row above the
                // hline in the final layout of this region, and thus no overlap
                // with the hline, allowing it to be drawn regardless of the
                // theoretical presence of a rowspan going across its position.
                let local_parent_y = rows
                    .iter()
                    .find(|row| row.y >= parent.y)
                    .map(|row| row.y)
                    .unwrap_or(y);

                if local_parent_y < y {
                    // There is a rowspan cell going through this hline's
                    // position, so don't draw it here.
                    return None;
                }
            }
        }
    }

    // When the hline is at the top of the region and this isn't the first
    // region, fold with the top stroke of the topmost cell at this column,
    // that is, the top border.
    let use_top_border_stroke = local_top_y.is_none() && y != 0;
    let (top_cell_stroke, top_cell_prioritized) = local_top_y
        .or(use_top_border_stroke.then_some(0))
        .and_then(|top_y| {
            // Let's find the parent cell of the position above us, in order
            // to take its bottom stroke, even when we're below gutter.
            grid.effective_parent_cell_position(x, top_y)
        })
        .map(|parent| {
            let top_cell = grid.cell(parent.x, parent.y).unwrap();
            if use_top_border_stroke {
                (top_cell.stroke.top.clone(), top_cell.stroke_overridden.top)
            } else {
                (top_cell.stroke.bottom.clone(), top_cell.stroke_overridden.bottom)
            }
        })
        .unwrap_or((None, false));

    // Use the bottom border stroke with priority if we're not in the last
    // region, we have the last index, and (as a failsafe) we don't have the
    // last row of cells above us.
    let use_bottom_border_stroke = !in_last_region
        && local_top_y.map_or(true, |top_y| top_y + 1 != grid.rows.len())
        && y == grid.rows.len();
    let bottom_y =
        if use_bottom_border_stroke { grid.rows.len().saturating_sub(1) } else { y };
    let (bottom_cell_stroke, bottom_cell_prioritized) = if bottom_y < grid.rows.len() {
        // Let's find the parent cell of the position below us, in order
        // to take its top stroke, even when we're above gutter.
        grid.effective_parent_cell_position(x, bottom_y)
            .map(|parent| {
                let bottom_cell = grid.cell(parent.x, parent.y).unwrap();
                if use_bottom_border_stroke {
                    (
                        bottom_cell.stroke.bottom.clone(),
                        bottom_cell.stroke_overridden.bottom,
                    )
                } else {
                    (bottom_cell.stroke.top.clone(), bottom_cell.stroke_overridden.top)
                }
            })
            .unwrap_or((None, false))
    } else {
        // No cell below the bottom border.
        (None, false)
    };

    let priority = if stroke.is_some() {
        StrokePriority::ExplicitLine
    } else if top_cell_prioritized || bottom_cell_prioritized {
        StrokePriority::CellStroke
    } else {
        StrokePriority::GridStroke
    };

    // Top border stroke and header stroke are generally prioritized, unless
    // they don't have explicit hline overrides and one or more user-provided
    // hlines would appear at the same position, which then are prioritized.
    let top_stroke_comes_from_header = grid
        .header
        .as_ref()
        .and_then(Repeatable::as_repeated)
        .zip(local_top_y)
        .is_some_and(|(header, local_top_y)| {
            // Ensure the row above us is a repeated header.
            // FIXME: Make this check more robust when headers at arbitrary
            // positions are added.
            local_top_y < header.end && y > header.end
        });

    // Prioritize the footer's top stroke as well where applicable.
    let bottom_stroke_comes_from_footer = grid
        .footer
        .as_ref()
        .and_then(Repeatable::as_repeated)
        .is_some_and(|footer| {
            // Ensure the row below us is a repeated footer.
            // FIXME: Make this check more robust when footers at arbitrary
            // positions are added.
            local_top_y.unwrap_or(0) + 1 < footer.start && y >= footer.start
        });

    let (prioritized_cell_stroke, deprioritized_cell_stroke) =
        if !use_bottom_border_stroke
            && !bottom_stroke_comes_from_footer
            && (use_top_border_stroke
                || top_stroke_comes_from_header
                || top_cell_prioritized && !bottom_cell_prioritized)
        {
            // Top border must always be prioritized, even if it did not
            // request for that explicitly.
            (top_cell_stroke, bottom_cell_stroke)
        } else {
            // When both cells' strokes have the same priority, we default to
            // prioritizing the bottom cell's top stroke.
            // Additionally, the bottom border cell's stroke always has
            // priority. Same for stroke above footers.
            (bottom_cell_stroke, top_cell_stroke)
        };

    // When both cells specify a stroke for this line segment, fold
    // both strokes, with priority to either the one prioritized cell,
    // or to the bottom cell's top stroke in case of a tie. But when one of
    // them doesn't specify a stroke, the other cell's stroke should be used
    // instead, regardless of priority (hence the usage of 'fold_or').
    let cell_stroke = prioritized_cell_stroke.fold_or(deprioritized_cell_stroke);

    // Fold the line stroke and folded cell strokes, if possible.
    // Give priority to the explicit line stroke.
    // Otherwise, use whichever of the two isn't 'none' or unspecified.
    let final_stroke = stroke.fold_or(Some(cell_stroke)).flatten();

    final_stroke.zip(Some(priority))
}

#[cfg(test)]
mod test {
    use std::num::NonZeroUsize;
    use typst_library::foundations::Content;
    use typst_library::introspection::Locator;
    use typst_library::layout::grid::resolve::{Cell, Entry, LinePosition};
    use typst_library::layout::{Axes, Sides, Sizing};
    use typst_utils::NonZeroExt;

    use super::*;

    fn sample_cell() -> Cell<'static> {
        Cell {
            body: Content::default(),
            locator: Locator::root(),
            fill: None,
            colspan: NonZeroUsize::ONE,
            rowspan: NonZeroUsize::ONE,
            stroke: Sides::splat(Some(Arc::new(Stroke::default()))),
            stroke_overridden: Sides::splat(false),
            breakable: true,
        }
    }

    fn cell_with_colspan_rowspan(colspan: usize, rowspan: usize) -> Cell<'static> {
        Cell {
            body: Content::default(),
            locator: Locator::root(),
            fill: None,
            colspan: NonZeroUsize::try_from(colspan).unwrap(),
            rowspan: NonZeroUsize::try_from(rowspan).unwrap(),
            stroke: Sides::splat(Some(Arc::new(Stroke::default()))),
            stroke_overridden: Sides::splat(false),
            breakable: true,
        }
    }

    fn sample_grid_for_vlines(gutters: bool) -> CellGrid<'static> {
        const COLS: usize = 4;
        const ROWS: usize = 6;
        let entries = vec![
            // row 0
            Entry::Cell(sample_cell()),
            Entry::Cell(sample_cell()),
            Entry::Cell(cell_with_colspan_rowspan(2, 1)),
            Entry::Merged { parent: 2 },
            // row 1
            Entry::Cell(sample_cell()),
            Entry::Cell(cell_with_colspan_rowspan(3, 1)),
            Entry::Merged { parent: 5 },
            Entry::Merged { parent: 5 },
            // row 2
            Entry::Merged { parent: 4 },
            Entry::Cell(sample_cell()),
            Entry::Cell(cell_with_colspan_rowspan(2, 1)),
            Entry::Merged { parent: 10 },
            // row 3
            Entry::Cell(sample_cell()),
            Entry::Cell(cell_with_colspan_rowspan(3, 2)),
            Entry::Merged { parent: 13 },
            Entry::Merged { parent: 13 },
            // row 4
            Entry::Cell(sample_cell()),
            Entry::Merged { parent: 13 },
            Entry::Merged { parent: 13 },
            Entry::Merged { parent: 13 },
            // row 5
            Entry::Cell(sample_cell()),
            Entry::Cell(sample_cell()),
            Entry::Cell(cell_with_colspan_rowspan(2, 1)),
            Entry::Merged { parent: 22 },
        ];
        CellGrid::new_internal(
            Axes::with_x(&[Sizing::Auto; COLS]),
            if gutters {
                Axes::new(&[Sizing::Auto; COLS - 1], &[Sizing::Auto; ROWS - 1])
            } else {
                Axes::default()
            },
            vec![],
            vec![],
            None,
            None,
            entries,
        )
    }

    #[test]
    fn test_vline_splitting_without_gutter() {
        let stroke = Arc::new(Stroke::default());
        let grid = sample_grid_for_vlines(false);
        let rows = &[
            RowPiece { height: Abs::pt(1.0), y: 0 },
            RowPiece { height: Abs::pt(2.0), y: 1 },
            RowPiece { height: Abs::pt(4.0), y: 2 },
            RowPiece { height: Abs::pt(8.0), y: 3 },
            RowPiece { height: Abs::pt(16.0), y: 4 },
            RowPiece { height: Abs::pt(32.0), y: 5 },
        ];
        let expected_vline_splits = &[
            vec![LineSegment {
                stroke: stroke.clone(),
                offset: Abs::pt(0.),
                length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32.),
                priority: StrokePriority::GridStroke,
            }],
            vec![LineSegment {
                stroke: stroke.clone(),
                offset: Abs::pt(0.),
                length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32.),
                priority: StrokePriority::GridStroke,
            }],
            // interrupted a few times by colspans
            vec![
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(0.),
                    length: Abs::pt(1.),
                    priority: StrokePriority::GridStroke,
                },
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(1. + 2.),
                    length: Abs::pt(4.),
                    priority: StrokePriority::GridStroke,
                },
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(1. + 2. + 4. + 8. + 16.),
                    length: Abs::pt(32.),
                    priority: StrokePriority::GridStroke,
                },
            ],
            // interrupted every time by colspans
            vec![],
            vec![LineSegment {
                stroke,
                offset: Abs::pt(0.),
                length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32.),
                priority: StrokePriority::GridStroke,
            }],
        ];
        for (x, expected_splits) in expected_vline_splits.iter().enumerate() {
            let tracks = rows.iter().map(|row| (row.y, row.height));
            assert_eq!(
                expected_splits,
                &generate_line_segments(&grid, tracks, x, &[], vline_stroke_at_row)
                    .collect::<Vec<_>>(),
            );
        }
    }

    #[test]
    fn test_vline_splitting_with_gutter_and_per_cell_stroke() {
        let stroke = Arc::new(Stroke::default());
        let grid = sample_grid_for_vlines(true);
        let rows = &[
            RowPiece { height: Abs::pt(1.0), y: 0 },
            RowPiece { height: Abs::pt(2.0), y: 1 },
            RowPiece { height: Abs::pt(4.0), y: 2 },
            RowPiece { height: Abs::pt(8.0), y: 3 },
            RowPiece { height: Abs::pt(16.0), y: 4 },
            RowPiece { height: Abs::pt(32.0), y: 5 },
            RowPiece { height: Abs::pt(64.0), y: 6 },
            RowPiece { height: Abs::pt(128.0), y: 7 },
            RowPiece { height: Abs::pt(256.0), y: 8 },
            RowPiece { height: Abs::pt(512.0), y: 9 },
            RowPiece { height: Abs::pt(1024.0), y: 10 },
        ];
        // Stroke is per-cell so we skip gutter
        let expected_vline_splits = &[
            // left border
            vec![
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(0.),
                    length: Abs::pt(1.),
                    priority: StrokePriority::GridStroke,
                },
                // Covers the rowspan between (original) rows 1 and 2
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(1. + 2.),
                    length: Abs::pt(4. + 8. + 16.),
                    priority: StrokePriority::GridStroke,
                },
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(1. + 2. + 4. + 8. + 16. + 32.),
                    length: Abs::pt(64.),
                    priority: StrokePriority::GridStroke,
                },
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64. + 128.),
                    length: Abs::pt(256.),
                    priority: StrokePriority::GridStroke,
                },
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(
                        1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256. + 512.,
                    ),
                    length: Abs::pt(1024.),
                    priority: StrokePriority::GridStroke,
                },
            ],
            // gutter line below
            vec![
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(0.),
                    length: Abs::pt(1.),
                    priority: StrokePriority::GridStroke,
                },
                // Covers the rowspan between (original) rows 1 and 2
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(1. + 2.),
                    length: Abs::pt(4. + 8. + 16.),
                    priority: StrokePriority::GridStroke,
                },
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(1. + 2. + 4. + 8. + 16. + 32.),
                    length: Abs::pt(64.),
                    priority: StrokePriority::GridStroke,
                },
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64. + 128.),
                    length: Abs::pt(256.),
                    priority: StrokePriority::GridStroke,
                },
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(
                        1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256. + 512.,
                    ),
                    length: Abs::pt(1024.),
                    priority: StrokePriority::GridStroke,
                },
            ],
            vec![
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(0.),
                    length: Abs::pt(1.),
                    priority: StrokePriority::GridStroke,
                },
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(1. + 2.),
                    length: Abs::pt(4.),
                    priority: StrokePriority::GridStroke,
                },
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(1. + 2. + 4. + 8.),
                    length: Abs::pt(16.),
                    priority: StrokePriority::GridStroke,
                },
                // Covers the rowspan between (original) rows 3 and 4
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(1. + 2. + 4. + 8. + 16. + 32.),
                    length: Abs::pt(64. + 128. + 256.),
                    priority: StrokePriority::GridStroke,
                },
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(
                        1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256. + 512.,
                    ),
                    length: Abs::pt(1024.),
                    priority: StrokePriority::GridStroke,
                },
            ],
            // gutter line below
            // the two lines below are interrupted multiple times by colspans
            vec![
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(0.),
                    length: Abs::pt(1.),
                    priority: StrokePriority::GridStroke,
                },
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(1. + 2. + 4. + 8.),
                    length: Abs::pt(16.),
                    priority: StrokePriority::GridStroke,
                },
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(
                        1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256. + 512.,
                    ),
                    length: Abs::pt(1024.),
                    priority: StrokePriority::GridStroke,
                },
            ],
            vec![
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(0.),
                    length: Abs::pt(1.),
                    priority: StrokePriority::GridStroke,
                },
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(1. + 2. + 4. + 8.),
                    length: Abs::pt(16.),
                    priority: StrokePriority::GridStroke,
                },
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(
                        1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256. + 512.,
                    ),
                    length: Abs::pt(1024.),
                    priority: StrokePriority::GridStroke,
                },
            ],
            // gutter line below
            // the two lines below can only cross certain gutter rows, because
            // all non-gutter cells in the following column are merged with
            // cells from the previous column.
            vec![],
            vec![],
            // right border
            vec![
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(0.),
                    length: Abs::pt(1.),
                    priority: StrokePriority::GridStroke,
                },
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(1. + 2.),
                    length: Abs::pt(4.),
                    priority: StrokePriority::GridStroke,
                },
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(1. + 2. + 4. + 8.),
                    length: Abs::pt(16.),
                    priority: StrokePriority::GridStroke,
                },
                // Covers the rowspan between (original) rows 3 and 4
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(1. + 2. + 4. + 8. + 16. + 32.),
                    length: Abs::pt(64. + 128. + 256.),
                    priority: StrokePriority::GridStroke,
                },
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(
                        1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256. + 512.,
                    ),
                    length: Abs::pt(1024.),
                    priority: StrokePriority::GridStroke,
                },
            ],
        ];
        for (x, expected_splits) in expected_vline_splits.iter().enumerate() {
            let tracks = rows.iter().map(|row| (row.y, row.height));
            assert_eq!(
                expected_splits,
                &generate_line_segments(&grid, tracks, x, &[], vline_stroke_at_row)
                    .collect::<Vec<_>>(),
            );
        }
    }

    #[test]
    fn test_vline_splitting_with_gutter_and_explicit_vlines() {
        let stroke = Arc::new(Stroke::default());
        let grid = sample_grid_for_vlines(true);
        let rows = &[
            RowPiece { height: Abs::pt(1.0), y: 0 },
            RowPiece { height: Abs::pt(2.0), y: 1 },
            RowPiece { height: Abs::pt(4.0), y: 2 },
            RowPiece { height: Abs::pt(8.0), y: 3 },
            RowPiece { height: Abs::pt(16.0), y: 4 },
            RowPiece { height: Abs::pt(32.0), y: 5 },
            RowPiece { height: Abs::pt(64.0), y: 6 },
            RowPiece { height: Abs::pt(128.0), y: 7 },
            RowPiece { height: Abs::pt(256.0), y: 8 },
            RowPiece { height: Abs::pt(512.0), y: 9 },
            RowPiece { height: Abs::pt(1024.0), y: 10 },
        ];
        let expected_vline_splits = &[
            // left border
            vec![LineSegment {
                stroke: stroke.clone(),
                offset: Abs::pt(0.),
                length: Abs::pt(
                    1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256. + 512. + 1024.,
                ),
                priority: StrokePriority::ExplicitLine,
            }],
            // gutter line below
            vec![LineSegment {
                stroke: stroke.clone(),
                offset: Abs::pt(0.),
                length: Abs::pt(
                    1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256. + 512. + 1024.,
                ),
                priority: StrokePriority::ExplicitLine,
            }],
            vec![LineSegment {
                stroke: stroke.clone(),
                offset: Abs::pt(0.),
                length: Abs::pt(
                    1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256. + 512. + 1024.,
                ),
                priority: StrokePriority::ExplicitLine,
            }],
            // gutter line below
            // the two lines below are interrupted multiple times by colspans
            vec![
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(0.),
                    length: Abs::pt(1. + 2.),
                    priority: StrokePriority::ExplicitLine,
                },
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(1. + 2. + 4.),
                    length: Abs::pt(8. + 16. + 32.),
                    priority: StrokePriority::ExplicitLine,
                },
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256.),
                    length: Abs::pt(512. + 1024.),
                    priority: StrokePriority::ExplicitLine,
                },
            ],
            vec![
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(0.),
                    length: Abs::pt(1. + 2.),
                    priority: StrokePriority::ExplicitLine,
                },
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(1. + 2. + 4.),
                    length: Abs::pt(8. + 16. + 32.),
                    priority: StrokePriority::ExplicitLine,
                },
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256.),
                    length: Abs::pt(512. + 1024.),
                    priority: StrokePriority::ExplicitLine,
                },
            ],
            // gutter line below
            // the two lines below can only cross certain gutter rows, because
            // all non-gutter cells in the following column are merged with
            // cells from the previous column.
            vec![
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(1.),
                    length: Abs::pt(2.),
                    priority: StrokePriority::ExplicitLine,
                },
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(1. + 2. + 4.),
                    length: Abs::pt(8.),
                    priority: StrokePriority::ExplicitLine,
                },
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(1. + 2. + 4. + 8. + 16.),
                    length: Abs::pt(32.),
                    priority: StrokePriority::ExplicitLine,
                },
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256.),
                    length: Abs::pt(512.),
                    priority: StrokePriority::ExplicitLine,
                },
            ],
            vec![
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(1.),
                    length: Abs::pt(2.),
                    priority: StrokePriority::ExplicitLine,
                },
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(1. + 2. + 4.),
                    length: Abs::pt(8.),
                    priority: StrokePriority::ExplicitLine,
                },
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(1. + 2. + 4. + 8. + 16.),
                    length: Abs::pt(32.),
                    priority: StrokePriority::ExplicitLine,
                },
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256.),
                    length: Abs::pt(512.),
                    priority: StrokePriority::ExplicitLine,
                },
            ],
            // right border
            vec![LineSegment {
                stroke: stroke.clone(),
                offset: Abs::pt(0.),
                length: Abs::pt(
                    1. + 2. + 4. + 8. + 16. + 32. + 64. + 128. + 256. + 512. + 1024.,
                ),
                priority: StrokePriority::ExplicitLine,
            }],
        ];
        for (x, expected_splits) in expected_vline_splits.iter().enumerate() {
            let tracks = rows.iter().map(|row| (row.y, row.height));
            assert_eq!(
                expected_splits,
                &generate_line_segments(
                    &grid,
                    tracks,
                    x,
                    &[
                        Line {
                            index: x,
                            start: 0,
                            end: None,
                            stroke: Some(stroke.clone()),
                            position: LinePosition::Before
                        },
                        Line {
                            index: x,
                            start: 0,
                            end: None,
                            stroke: Some(stroke.clone()),
                            position: LinePosition::After
                        },
                    ],
                    vline_stroke_at_row
                )
                .collect::<Vec<_>>(),
            );
        }
    }

    fn sample_grid_for_hlines(gutters: bool) -> CellGrid<'static> {
        const COLS: usize = 4;
        const ROWS: usize = 9;
        let entries = vec![
            // row 0
            Entry::Cell(cell_with_colspan_rowspan(1, 2)),
            Entry::Cell(sample_cell()),
            Entry::Cell(cell_with_colspan_rowspan(2, 2)),
            Entry::Merged { parent: 2 },
            // row 1
            Entry::Merged { parent: 0 },
            Entry::Cell(sample_cell()),
            Entry::Merged { parent: 2 },
            Entry::Merged { parent: 2 },
            // row 2
            Entry::Cell(sample_cell()),
            Entry::Cell(sample_cell()),
            Entry::Cell(sample_cell()),
            Entry::Cell(sample_cell()),
            // row 3
            Entry::Cell(cell_with_colspan_rowspan(4, 2)),
            Entry::Merged { parent: 12 },
            Entry::Merged { parent: 12 },
            Entry::Merged { parent: 12 },
            // row 4
            Entry::Merged { parent: 12 },
            Entry::Merged { parent: 12 },
            Entry::Merged { parent: 12 },
            Entry::Merged { parent: 12 },
            // row 5
            Entry::Cell(sample_cell()),
            Entry::Cell(cell_with_colspan_rowspan(1, 2)),
            Entry::Cell(cell_with_colspan_rowspan(2, 1)),
            Entry::Merged { parent: 22 },
            // row 6
            Entry::Cell(sample_cell()),
            Entry::Merged { parent: 21 },
            Entry::Cell(sample_cell()),
            Entry::Cell(sample_cell()),
            // row 7 (adjacent rowspans covering the whole row)
            Entry::Cell(cell_with_colspan_rowspan(2, 2)),
            Entry::Merged { parent: 28 },
            Entry::Cell(cell_with_colspan_rowspan(2, 2)),
            Entry::Merged { parent: 30 },
            // row 8
            Entry::Merged { parent: 28 },
            Entry::Merged { parent: 28 },
            Entry::Merged { parent: 30 },
            Entry::Merged { parent: 30 },
        ];
        CellGrid::new_internal(
            Axes::with_x(&[Sizing::Auto; COLS]),
            if gutters {
                Axes::new(&[Sizing::Auto; COLS - 1], &[Sizing::Auto; ROWS - 1])
            } else {
                Axes::default()
            },
            vec![],
            vec![],
            None,
            None,
            entries,
        )
    }

    #[test]
    fn test_hline_splitting_without_gutter() {
        let stroke = Arc::new(Stroke::default());
        let grid = sample_grid_for_hlines(false);
        let columns = &[Abs::pt(1.), Abs::pt(2.), Abs::pt(4.), Abs::pt(8.)];
        // Assume all rows would be drawn in the same region, and are available.
        let rows = grid
            .rows
            .iter()
            .enumerate()
            .map(|(y, _)| RowPiece { height: Abs::pt(f64::from(2u32.pow(y as u32))), y })
            .collect::<Vec<_>>();
        let expected_hline_splits = &[
            // top border
            vec![LineSegment {
                stroke: stroke.clone(),
                offset: Abs::pt(0.),
                length: Abs::pt(1. + 2. + 4. + 8.),
                priority: StrokePriority::GridStroke,
            }],
            // interrupted a few times by rowspans
            vec![LineSegment {
                stroke: stroke.clone(),
                offset: Abs::pt(1.),
                length: Abs::pt(2.),
                priority: StrokePriority::GridStroke,
            }],
            vec![LineSegment {
                stroke: stroke.clone(),
                offset: Abs::pt(0.),
                length: Abs::pt(1. + 2. + 4. + 8.),
                priority: StrokePriority::GridStroke,
            }],
            vec![LineSegment {
                stroke: stroke.clone(),
                offset: Abs::pt(0.),
                length: Abs::pt(1. + 2. + 4. + 8.),
                priority: StrokePriority::GridStroke,
            }],
            // interrupted every time by rowspans
            vec![],
            vec![LineSegment {
                stroke: stroke.clone(),
                offset: Abs::pt(0.),
                length: Abs::pt(1. + 2. + 4. + 8.),
                priority: StrokePriority::GridStroke,
            }],
            // interrupted once by rowspan
            vec![
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(0.),
                    length: Abs::pt(1.),
                    priority: StrokePriority::GridStroke,
                },
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(1. + 2.),
                    length: Abs::pt(4. + 8.),
                    priority: StrokePriority::GridStroke,
                },
            ],
            vec![LineSegment {
                stroke: stroke.clone(),
                offset: Abs::pt(0.),
                length: Abs::pt(1. + 2. + 4. + 8.),
                priority: StrokePriority::GridStroke,
            }],
            // interrupted every time by successive rowspans
            vec![],
            // bottom border
            vec![LineSegment {
                stroke,
                offset: Abs::pt(0.),
                length: Abs::pt(1. + 2. + 4. + 8.),
                priority: StrokePriority::GridStroke,
            }],
        ];
        for (y, expected_splits) in expected_hline_splits.iter().enumerate() {
            let tracks = columns.iter().copied().enumerate();
            assert_eq!(
                expected_splits,
                &generate_line_segments(&grid, tracks, y, &[], |grid, y, x, stroke| {
                    hline_stroke_at_column(
                        grid,
                        &rows,
                        y.checked_sub(1),
                        true,
                        y,
                        x,
                        stroke,
                    )
                })
                .collect::<Vec<_>>(),
            );
        }
    }

    #[test]
    fn test_hline_splitting_with_gutter_and_explicit_hlines() {
        let stroke = Arc::new(Stroke::default());
        let grid = sample_grid_for_hlines(true);
        let columns = &[
            Abs::pt(1.0),
            Abs::pt(2.0),
            Abs::pt(4.0),
            Abs::pt(8.0),
            Abs::pt(16.0),
            Abs::pt(32.0),
            Abs::pt(64.0),
        ];
        // Assume all rows would be drawn in the same region, and are available.
        let rows = grid
            .rows
            .iter()
            .enumerate()
            .map(|(y, _)| RowPiece { height: Abs::pt(f64::from(2u32.pow(y as u32))), y })
            .collect::<Vec<_>>();
        let expected_hline_splits = &[
            // top border
            vec![LineSegment {
                stroke: stroke.clone(),
                offset: Abs::pt(0.),
                length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64.),
                priority: StrokePriority::ExplicitLine,
            }],
            // gutter line below
            // interrupted a few times by rowspans
            vec![LineSegment {
                stroke: stroke.clone(),
                offset: Abs::pt(1.),
                length: Abs::pt(2. + 4. + 8.),
                priority: StrokePriority::ExplicitLine,
            }],
            // interrupted a few times by rowspans
            vec![LineSegment {
                stroke: stroke.clone(),
                offset: Abs::pt(1.),
                length: Abs::pt(2. + 4. + 8.),
                priority: StrokePriority::ExplicitLine,
            }],
            // gutter line below
            vec![LineSegment {
                stroke: stroke.clone(),
                offset: Abs::pt(0.),
                length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64.),
                priority: StrokePriority::ExplicitLine,
            }],
            vec![LineSegment {
                stroke: stroke.clone(),
                offset: Abs::pt(0.),
                length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64.),
                priority: StrokePriority::ExplicitLine,
            }],
            // gutter line below
            vec![LineSegment {
                stroke: stroke.clone(),
                offset: Abs::pt(0.),
                length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64.),
                priority: StrokePriority::ExplicitLine,
            }],
            vec![LineSegment {
                stroke: stroke.clone(),
                offset: Abs::pt(0.),
                length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64.),
                priority: StrokePriority::ExplicitLine,
            }],
            // gutter line below
            // interrupted every time by rowspans
            vec![],
            // interrupted every time by rowspans
            vec![],
            // gutter line below
            vec![LineSegment {
                stroke: stroke.clone(),
                offset: Abs::pt(0.),
                length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64.),
                priority: StrokePriority::ExplicitLine,
            }],
            vec![LineSegment {
                stroke: stroke.clone(),
                offset: Abs::pt(0.),
                length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64.),
                priority: StrokePriority::ExplicitLine,
            }],
            // gutter line below
            // interrupted once by rowspan
            vec![
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(0.),
                    length: Abs::pt(1. + 2.),
                    priority: StrokePriority::ExplicitLine,
                },
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(1. + 2. + 4.),
                    length: Abs::pt(8. + 16. + 32. + 64.),
                    priority: StrokePriority::ExplicitLine,
                },
            ],
            // interrupted once by rowspan
            vec![
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(0.),
                    length: Abs::pt(1. + 2.),
                    priority: StrokePriority::ExplicitLine,
                },
                LineSegment {
                    stroke: stroke.clone(),
                    offset: Abs::pt(1. + 2. + 4.),
                    length: Abs::pt(8. + 16. + 32. + 64.),
                    priority: StrokePriority::ExplicitLine,
                },
            ],
            // gutter line below
            vec![LineSegment {
                stroke: stroke.clone(),
                offset: Abs::pt(0.),
                length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64.),
                priority: StrokePriority::ExplicitLine,
            }],
            vec![LineSegment {
                stroke: stroke.clone(),
                offset: Abs::pt(0.),
                length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64.),
                priority: StrokePriority::ExplicitLine,
            }],
            // gutter line below
            // there are two consecutive rowspans, but the gutter column
            // between them is free.
            vec![LineSegment {
                stroke: stroke.clone(),
                offset: Abs::pt(1. + 2. + 4.),
                length: Abs::pt(8.),
                priority: StrokePriority::ExplicitLine,
            }],
            vec![LineSegment {
                stroke: stroke.clone(),
                offset: Abs::pt(1. + 2. + 4.),
                length: Abs::pt(8.),
                priority: StrokePriority::ExplicitLine,
            }],
            // bottom border
            vec![LineSegment {
                stroke: stroke.clone(),
                offset: Abs::pt(0.),
                length: Abs::pt(1. + 2. + 4. + 8. + 16. + 32. + 64.),
                priority: StrokePriority::ExplicitLine,
            }],
        ];
        for (y, expected_splits) in expected_hline_splits.iter().enumerate() {
            let tracks = columns.iter().copied().enumerate();
            assert_eq!(
                expected_splits,
                &generate_line_segments(
                    &grid,
                    tracks,
                    y,
                    &[
                        Line {
                            index: y,
                            start: 0,
                            end: None,
                            stroke: Some(stroke.clone()),
                            position: LinePosition::Before
                        },
                        Line {
                            index: y,
                            start: 0,
                            end: None,
                            stroke: Some(stroke.clone()),
                            position: LinePosition::After
                        },
                    ],
                    |grid, y, x, stroke| hline_stroke_at_column(
                        grid,
                        &rows,
                        y.checked_sub(1),
                        true,
                        y,
                        x,
                        stroke
                    )
                )
                .collect::<Vec<_>>(),
            );
        }
    }

    #[test]
    fn test_hline_splitting_considers_absent_rows() {
        let grid = sample_grid_for_hlines(false);
        let columns = &[Abs::pt(1.), Abs::pt(2.), Abs::pt(4.), Abs::pt(8.)];
        // Assume row 3 is absent (even though there's a rowspan between rows
        // 3 and 4)
        // This can happen if it is an auto row which turns out to be fully
        // empty.
        let rows = grid
            .rows
            .iter()
            .enumerate()
            .filter(|(y, _)| *y != 3)
            .map(|(y, _)| RowPiece { height: Abs::pt(f64::from(2u32.pow(y as u32))), y })
            .collect::<Vec<_>>();

        // Hline above row 4 is no longer blocked, since the rowspan is now
        // effectively spanning just one row (at least, visibly).
        assert_eq!(
            &vec![LineSegment {
                stroke: Arc::new(Stroke::default()),
                offset: Abs::pt(0.),
                length: Abs::pt(1. + 2. + 4. + 8.),
                priority: StrokePriority::GridStroke
            }],
            &generate_line_segments(
                &grid,
                columns.iter().copied().enumerate(),
                4,
                &[],
                |grid, y, x, stroke| hline_stroke_at_column(
                    grid,
                    &rows,
                    if y == 4 { Some(2) } else { y.checked_sub(1) },
                    true,
                    y,
                    x,
                    stroke
                )
            )
            .collect::<Vec<_>>()
        );
    }
}
