mod types;
mod table;
pub use types::*;
pub use table::{edge_key, border_side};
use table::{layout_table_with_break, row_span_groups, build_header_table};

use crate::{
    error::Result,
    measure::{MeasuredBlock, MeasuredDocument, MeasuredTable},
    model::{Alignment, Document, HeaderFooter, Margins, PageSize, WatermarkFontSize},
    text::{InlineLine, InlineRun},
};

mod absolute;
mod columns;
mod helpers;
mod list;
mod watermark;

pub(super) fn ensure_block_fits(pages: &mut Vec<LayoutPage>, y: &mut f32, height: f32, top: f32, bottom_limit: f32, unbreakable: bool) {
    if *y + height > bottom_limit {
        if unbreakable || !pages.last().unwrap().items.is_empty() {
            pages.push(LayoutPage { items: Vec::new(), background: None, watermark: None, header: None, footer: None, custom_properties: PageCustomProperties::default() });
            *y = top;
        }
    }
}

/// Layout with font-aware watermark measurement.
pub fn layout_document_with_fonts(document: &MeasuredDocument, fonts: &crate::font::FontRegistry, default_font_family: Option<&str>) -> Result<LayoutDocument> {
    layout_document_with_callback(document, None::<&dyn Fn(usize, usize) -> bool>, fonts, default_font_family)
}

/// Layout with default settings (Helvetica watermark, no custom font).
pub fn layout_document(document: &MeasuredDocument) -> Result<LayoutDocument> {
    layout_document_with_fonts(document, &crate::font::FontRegistry::empty(), None)
}

pub fn layout_document_with_callback(
    document: &MeasuredDocument,
    page_break_before: Option<&dyn Fn(usize, usize) -> bool>,
    fonts: &crate::font::FontRegistry,
    default_font_family: Option<&str>,
) -> Result<LayoutDocument> {
    let mut pages = vec![LayoutPage { items: Vec::new(), background: None, watermark: None, header: None, footer: None, custom_properties: PageCustomProperties::default() }];
    let mut y = document.page_margins.top;
    // When auto_height is true, use a very large bottom_limit to prevent pagination
    let bottom_limit = if document.page_size.auto_height {
        f32::MAX / 2.0
    } else {
        document.page_size.height - document.page_margins.bottom
    };
    let left = document.page_margins.left;
    let available_width = document.page_size.width - document.page_margins.left - document.page_margins.right;

    for (block_idx, block) in document.blocks.iter().enumerate() {
        let page_break = match block {
            MeasuredBlock::Text(t) => t.page_break,
            MeasuredBlock::Table(t) => t.page_break,
            _ => None,
        };

        if let Some(ref callback) = page_break_before {
            let origin_idx = match block {
                MeasuredBlock::Text(t) => t.origin_node_idx,
                MeasuredBlock::Table(t) => t.origin_node_idx,
                MeasuredBlock::Image(i) => i.origin_node_idx,
                MeasuredBlock::Qr(q) => q.origin_node_idx,
                MeasuredBlock::Svg(s) => s.origin_node_idx,
                MeasuredBlock::Columns(c) => c.origin_node_idx,
                MeasuredBlock::Absolute(a) => a.origin_node_idx,
                MeasuredBlock::List(l) => l.origin_node_idx,
                MeasuredBlock::Section(s) => s.origin_node_idx,
                MeasuredBlock::Canvas(_) => None,
                MeasuredBlock::Toc(_) => None,
            };
            if let Some(_node_idx) = origin_idx {
                let current_page = pages.len();
                if callback(block_idx, current_page) {
                    // Always create a new page when callback returns true
                    if !pages.last().unwrap().items.is_empty() {
                        pages.push(LayoutPage { items: Vec::new(), background: None, watermark: None, header: None, footer: None, custom_properties: PageCustomProperties::default() });
                    }
                    y = document.page_margins.top;
                }
            }
        }

        helpers::apply_page_break_before(page_break, &mut pages, &mut y, document.page_margins.top);

        match block {
            MeasuredBlock::Columns(cols) => {
                if cols.snaking_columns {
                    columns::layout_snaking_columns(cols, left, &mut y, available_width, bottom_limit, document.page_margins.top, &mut pages);
                } else {
                    ensure_block_fits(&mut pages, &mut y, cols.height, document.page_margins.top, bottom_limit, false);

                    let column_count = cols.columns.len() as f32;
                    let column_width = if column_count > 1.0 {
                        (available_width - cols.column_gap * (column_count - 1.0)) / column_count
                    } else {
                        available_width
                    };

                    let start_y = y;
                    let mut layout_columns: Vec<Vec<LayoutItem>> = Vec::new();

                    for (col_idx, col_blocks) in cols.columns.iter().enumerate() {
                        let col_x = left + col_idx as f32 * (column_width + cols.column_gap);
                        let mut col_y = start_y;
                        let mut temp_pages = vec![LayoutPage { items: Vec::new(), background: None, watermark: None, header: None, footer: None, custom_properties: PageCustomProperties::default() }];

                        for block in col_blocks {
                            layout_block(block, col_x, &mut col_y, column_width, bottom_limit, document.page_margins.top, &mut temp_pages);
                        }

                        let mut col_items = Vec::new();
                        for page in &temp_pages {
                            col_items.extend(page.items.clone());
                        }

                        layout_columns.push(col_items);
                    }

                    pages.last_mut().unwrap().items.push(LayoutItem::Columns(LayoutColumns {
                        x: left,
                        y: start_y,
                        width: available_width,
                        height: cols.height,
                        columns: layout_columns,
                    }));
                    y += cols.height;
                }
            }
            MeasuredBlock::List(list_block) => {
                list::layout_list_block(list_block, left, &mut y, available_width, bottom_limit, document.page_margins.top, &mut pages);
            }
            MeasuredBlock::Absolute(abs) => {
                let (resolved_x, resolved_y) = absolute::resolve_absolute_position(abs, &document.page_size);
                let items = absolute::layout_absolute_block(abs, resolved_x, resolved_y);
                pages.last_mut().unwrap().items.extend(items);
            }
            _ => {
                layout_block(block, left, &mut y, available_width, bottom_limit, document.page_margins.top, &mut pages);
            }
        }

        helpers::apply_page_break_after(page_break, &mut pages, &mut y, document.page_margins.top);
    }

    if let Some(ref wm) = document.watermark {
        let ps = &document.page_size;
        let angle = wm.angle.unwrap_or(
            -(ps.height.atan2(ps.width) * 180.0 / std::f32::consts::PI)
        );
        let (cr, cg, cb) = wm.color.as_ref()
            .and_then(|c| crate::color::color_to_pdf_rgb(c))
            .unwrap_or((0.0, 0.0, 0.0));
        let opacity = wm.opacity.unwrap_or(0.6);

        // Resolve font: watermark.font_family > document default > Helvetica
        let resolved_font = wm.font_family.as_deref()
            .or(default_font_family);

        // Measure text width using the resolved font
        let text_width_fn = |font_size: f32| -> f32 {
            if let Some(family) = resolved_font {
                if let Ok(w) = fonts.measure(family, &wm.text, font_size) {
                    return w;
                }
            }
            crate::text::measure_helvetica(&wm.text, font_size)
        };

        let font_size = match &wm.font_size {
            Some(WatermarkFontSize::Fixed(size)) => *size,
            Some(WatermarkFontSize::Auto) | None => {
                watermark::auto_watermark_font_size_with(&wm.text, angle, ps, &text_width_fn)
            }
        };
        let text_width = text_width_fn(font_size);

        let layout_wm = LayoutWatermark {
            text: wm.text.clone(),
            font_family: resolved_font.map(|s| s.to_string()),
            font_size, color_r: cr, color_g: cg, color_b: cb,
            opacity, angle, text_width,
        };
        for page in &mut pages {
            page.watermark = Some(layout_wm.clone());
        }
    }

    // When auto_height is true, adjust page height to fit all content
    let page_size = if document.page_size.auto_height {
        let content_height = y + document.page_margins.bottom;
        PageSize { width: document.page_size.width, height: content_height, auto_height: true }
    } else {
        document.page_size
    };

    Ok(LayoutDocument { page_size, page_margins: document.page_margins, fonts: document.fonts.clone(), pages })
}

pub fn layout_header_footer(
    measured_blocks: &[MeasuredBlock],
    x: f32,
    y: f32,
    available_width: f32,
    height_limit: f32,
    _page_size: &PageSize,
    _page_margins: &Margins,
) -> Vec<LayoutItem> {
    let mut temp_pages = vec![LayoutPage { items: Vec::new(), background: None, watermark: None, header: None, footer: None, custom_properties: PageCustomProperties::default() }];
    let mut current_y = y;
    let bottom_limit = y + height_limit;
    for block in measured_blocks {
        layout_block(block, x, &mut current_y, available_width, bottom_limit, y, &mut temp_pages);
        if current_y > bottom_limit {
            break;
        }
    }
    temp_pages.into_iter().flat_map(|p| p.items).collect()
}

#[allow(dead_code)]
pub fn inject_header_footer(
    pages: &mut Vec<LayoutPage>,
    header_items: &[LayoutItem],
    footer_items: &[LayoutItem],
) {
    let header = if header_items.is_empty() {
        None
    } else {
        Some(LayoutHeaderFooter { items: header_items.to_vec() })
    };
    let footer = if footer_items.is_empty() {
        None
    } else {
        Some(LayoutHeaderFooter { items: footer_items.to_vec() })
    };
    for page in pages.iter_mut() {
        page.header = header.clone();
        page.footer = footer.clone();
    }
}

pub fn inject_header_footer_with_custom_properties(
    pages: &mut Vec<LayoutPage>,
    document: &Document,
    layout_fn: &dyn Fn(&HeaderFooter, &Document, u32, f32, f32, f32) -> Result<Vec<LayoutItem>>,
) -> Result<()> {
    for (i, page) in pages.iter_mut().enumerate() {
        let page_num = (i + 1) as u32;

        // Check custom properties first, then fall back to document defaults
        let header_hf = page.custom_properties.header.as_ref()
            .or(document.header.as_ref());

        if let Some(header) = header_hf {
            let items = layout_fn(
                header,
                document,
                page_num,
                document.page_margins.left,
                0.0,
                document.page_margins.top,
            )?;
            if !items.is_empty() {
                page.header = Some(LayoutHeaderFooter { items });
            }
        }

        let footer_hf = page.custom_properties.footer.as_ref()
            .or(document.footer.as_ref());

        if let Some(footer) = footer_hf {
            let footer_y = document.page_size.height - document.page_margins.bottom;
            let items = layout_fn(
                footer,
                document,
                page_num,
                document.page_margins.left,
                footer_y,
                document.page_margins.bottom,
            )?;
            if !items.is_empty() {
                page.footer = Some(LayoutHeaderFooter { items });
            }
        }
    }

    Ok(())
}

pub(super) fn layout_block(
    block: &MeasuredBlock,
    x: f32,
    y: &mut f32,
    available_width: f32,
    bottom_limit: f32,
    page_margins_top: f32,
    pages: &mut Vec<LayoutPage>,
) {
    match block {
        MeasuredBlock::Text(text) => {
            let margin = text.style.margin.unwrap_or(Margins::all(0.0));
            let layout_x = x + margin.left;
            let layout_available_width = (available_width - margin.left - margin.right).max(0.0);
            let leading_indent = text.style.leading_indent;
            *y += margin.top;

            if text.style.unbreakable {
                let total_height = text.height + margin.bottom;
                if *y + total_height > bottom_limit && !pages.last().unwrap().items.is_empty() {
                    *y -= margin.top;
                    pages.push(LayoutPage { items: Vec::new(), background: None, watermark: None, header: None, footer: None, custom_properties: PageCustomProperties::default() });
                    *y = page_margins_top;
                    *y += margin.top;
                }
            }

            for (line_idx, line) in text.lines.iter().enumerate() {
                if *y + line.line_height > bottom_limit && !pages.last().unwrap().items.is_empty() {
                    pages.push(LayoutPage { items: Vec::new(), background: None, watermark: None, header: None, footer: None, custom_properties: PageCustomProperties::default() });
                    *y = page_margins_top;
                }

                let alignment = line_alignment(line);
                let is_last_line = line_idx == text.lines.len() - 1;
                let is_justify = alignment == Alignment::Justify && !is_last_line;

                // Apply leading_indent to first line only
                let indent = if line_idx == 0 { leading_indent } else { 0.0 };

                let line_x = match alignment {
                    Alignment::Left | Alignment::Justify => layout_x + indent,
                    Alignment::Center => layout_x + (layout_available_width - line.width) / 2.0,
                    Alignment::Right => layout_x + layout_available_width - line.width,
                };

                pages.last_mut().unwrap().items.push(LayoutItem::Text(layout_text_line(line, line_x, *y, layout_available_width, is_justify, text.id.clone(), text.page_reference.clone())));
                *y += line.line_height;
            }

            *y += margin.bottom;
        }
        MeasuredBlock::Table(table) => {
            let groups = row_span_groups(table);
            let mut force_keep = table.keep_with_header_rows.is_some() && table.header_rows.is_some();
            let mut accumulated_data_groups: Vec<Vec<usize>> = Vec::new();
            let total_groups = groups.len();
            let mut has_break = false;

            for (gi, group) in groups.iter().enumerate() {
                let group_height: f32 = group.iter().map(|&idx| table.rows[idx].height).sum();
                let is_last_group = gi == total_groups - 1;
                let is_first_group = gi == 0;

                if *y + group_height > bottom_limit && !pages.last().unwrap().items.is_empty() {
                    has_break = true;
                    if force_keep {
                        accumulated_data_groups.push(group.clone());
                        let total_data_rows: usize = accumulated_data_groups.iter().map(|g| g.len()).sum();
                        let keep_n = table.keep_with_header_rows.unwrap();
                        if total_data_rows >= keep_n {
                            let header_table = build_header_table(table, &accumulated_data_groups);
                            pages.last_mut().unwrap().items.push(LayoutItem::Table(layout_table_with_break(&header_table, x, *y, true, is_last_group)));
                            *y += header_table.height;
                            accumulated_data_groups.clear();
                            force_keep = false;
                        }
                        continue;
                    }

                    pages.push(LayoutPage { items: Vec::new(), background: None, watermark: None, header: None, footer: None, custom_properties: PageCustomProperties::default() });
                    *y = page_margins_top;

                    if let Some(hr) = table.header_rows {
                        if gi > 0 {
                            let header_indices: Vec<usize> = (0..hr).collect();
                            let mut full_group = header_indices;
                            full_group.extend(group);
                            let new_height: f32 = full_group.iter().map(|&idx| table.rows[idx].height).sum();
                            let group_table = MeasuredTable {
                                column_widths: table.column_widths.clone(),
                                rows: full_group.iter().map(|&idx| table.rows[idx].clone()).collect(),
                                style: table.style.clone(),
                                height: new_height,
                                header_rows: None,
                                dont_break_rows: table.dont_break_rows,
                                page_break: None,
                                heights: table.heights.clone(),
                                keep_with_header_rows: table.keep_with_header_rows,
                                table_style: table.table_style.clone(),
                                layout_config: table.layout_config.clone(),
                                h_line_when_broken: table.h_line_when_broken,
                                origin_node_idx: table.origin_node_idx,
                            };
                            pages.last_mut().unwrap().items.push(LayoutItem::Table(layout_table_with_break(&group_table, x, *y, true, is_last_group)));
                            *y += new_height;
                            continue;
                        }
                    }
                }

                if force_keep {
                    accumulated_data_groups.push(group.clone());
                    let total_data_rows: usize = accumulated_data_groups.iter().map(|g| g.len()).sum();
                    let keep_n = table.keep_with_header_rows.unwrap();
                    if total_data_rows >= keep_n {
                        let header_table = build_header_table(table, &accumulated_data_groups);
                        pages.last_mut().unwrap().items.push(LayoutItem::Table(layout_table_with_break(&header_table, x, *y, is_first_group, is_last_group)));
                        *y += header_table.height;
                        accumulated_data_groups.clear();
                        force_keep = false;
                    }
                } else {
                    let group_table = MeasuredTable {
                        column_widths: table.column_widths.clone(),
                        rows: group.iter().map(|&idx| table.rows[idx].clone()).collect(),
                        style: table.style.clone(),
                        height: group_height,
                        header_rows: None,
                        dont_break_rows: table.dont_break_rows,
                        page_break: None,
                        heights: table.heights.clone(),
                        keep_with_header_rows: table.keep_with_header_rows,
                        table_style: table.table_style.clone(),
                        layout_config: table.layout_config.clone(),
                        h_line_when_broken: table.h_line_when_broken,
                        origin_node_idx: table.origin_node_idx,
                    };
                    pages.last_mut().unwrap().items.push(LayoutItem::Table(layout_table_with_break(&group_table, x, *y, is_first_group && !has_break, is_last_group)));
                    *y += group_height;
                }
            }
        }
        MeasuredBlock::Image(image) => {
            ensure_block_fits(pages, y, image.height, page_margins_top, bottom_limit, false);
            let img_x = helpers::align_block_x(x, available_width, image.width, image.alignment);
            pages.last_mut().unwrap().items.push(LayoutItem::Image(LayoutImage {
                x: img_x,
                y: *y,
                width: image.width,
                height: image.height,
                data: image.data.clone(),
                format: image.format,
                intrinsic_width: image.intrinsic_width,
                intrinsic_height: image.intrinsic_height,
                cover: image.cover,
                opacity: image.opacity,
                link: image.link.clone(),
                link_to_page: image.link_to_page,
                link_to_destination: image.link_to_destination.clone(),
            }));
            *y += image.height;
        }
        MeasuredBlock::Qr(qr) => {
            ensure_block_fits(pages, y, qr.height, page_margins_top, bottom_limit, false);
            let qr_x = helpers::align_block_x(x, available_width, qr.width, qr.alignment);
            pages.last_mut().unwrap().items.push(LayoutItem::Qr(LayoutQr {
                x: qr_x,
                y: *y,
                width: qr.width,
                height: qr.height,
                modules: qr.modules,
                dark_modules: qr.dark_modules.clone(),
                foreground: qr.foreground.clone(),
                background: qr.background.clone(),
                padding: qr.padding,
            }));
            *y += qr.height;
        }
        MeasuredBlock::Svg(svg) => {
            ensure_block_fits(pages, y, svg.height, page_margins_top, bottom_limit, false);
            let svg_x = helpers::align_block_x(x, available_width, svg.width, svg.alignment);
            pages.last_mut().unwrap().items.push(LayoutItem::Svg(LayoutSvg {
                x: svg_x,
                y: *y,
                width: svg.width,
                height: svg.height,
                view_width: svg.view_width,
                view_height: svg.view_height,
                shapes: svg.shapes.clone(),
                link: svg.link.clone(),
                link_to_page: svg.link_to_page,
                link_to_destination: svg.link_to_destination.clone(),
            }));
            *y += svg.height;
        }
        MeasuredBlock::Columns(_) => {}
        MeasuredBlock::Absolute(_) => {}
        MeasuredBlock::List(_) => {}
        MeasuredBlock::Section(section) => {
            if !pages.last().unwrap().items.is_empty() {
                pages.push(LayoutPage { items: Vec::new(), background: None, watermark: None, header: None, footer: None, custom_properties: PageCustomProperties::default() });
                *y = page_margins_top;
            }

            let current_page = pages.last_mut().unwrap();
            if section.header.is_some() || section.footer.is_some() {
                current_page.custom_properties = PageCustomProperties {
                    header: section.header.clone(),
                    footer: section.footer.clone(),
                };
            }

            for inner in &section.blocks {
                layout_block(inner, x, y, available_width, bottom_limit, page_margins_top, pages);
            }
        }
        MeasuredBlock::Canvas(canvas) => {
            ensure_block_fits(pages, y, canvas.height, page_margins_top, bottom_limit, false);
            pages.last_mut().unwrap().items.push(LayoutItem::Canvas(LayoutCanvas {
                x,
                y: *y,
                width: canvas.width,
                height: canvas.height,
                shapes: canvas.shapes.clone(),
            }));
            *y += canvas.height;
        }
        MeasuredBlock::Toc(toc) => {
            let mut title_items = Vec::new();
            if let Some(ref title) = toc.title {
                let prev_count = pages.last().unwrap().items.len();
                layout_block(title, x, y, available_width, bottom_limit, page_margins_top, pages);
                // Collect the title items that were just added, then remove from page
                let last_page = pages.last().unwrap();
                let new_count = last_page.items.len();
                if new_count > prev_count {
                    title_items.extend(last_page.items[prev_count..new_count].iter().cloned());
                    // Remove the duplicate items from the page (they'll be rendered via LayoutToc)
                    pages.last_mut().unwrap().items.truncate(prev_count);
                }
            }

            let mut entry_items = Vec::new();
            let entry_line_height = crate::text::line_height(&toc.text_style);
            for entry in &toc.entries {
                if *y + entry_line_height > bottom_limit && !pages.last().unwrap().items.is_empty() {
                    pages.push(LayoutPage { items: Vec::new(), background: None, watermark: None, header: None, footer: None, custom_properties: PageCustomProperties::default() });
                    *y = page_margins_top;
                }

                entry_items.push(LayoutTocEntry {
                    text: entry.text.clone(),
                    style: entry.style.clone(),
                    page_number: entry.page_number,
                    x,
                    y: *y,
                    line_height: entry_line_height,
                });
                *y += entry_line_height;
            }

            let total_height = toc.title_height + (toc.entries.len() as f32 * entry_line_height);
            pages.last_mut().unwrap().items.push(LayoutItem::Toc(LayoutToc {
                x,
                y: *y - total_height,
                width: available_width,
                height: total_height,
                id: toc.id.clone(),
                title: title_items,
                entries: entry_items,
                text_style: toc.text_style.clone(),
                number_style: toc.number_style.clone(),
            }));
        }
    }
}

pub(super) fn line_alignment(line: &InlineLine) -> Alignment {
    line.runs.first().map(|run| run.style.alignment).unwrap_or(Alignment::Left)
}

pub(super) fn layout_text_line(line: &InlineLine, x: f32, y: f32, available_width: f32, is_justify: bool, id: Option<String>, page_reference: Option<String>) -> LayoutTextItem {
    let mut x_offset = 0.0;
    let mut runs = Vec::new();
    for run in &line.runs {
        let mut lr = layout_run(run, x_offset);
        lr.is_justify = is_justify;
        lr.available_width = available_width;
        runs.push(lr);
        x_offset += run.width;
    }

    LayoutTextItem {
        text: line.runs.iter().map(|run| run.text.as_str()).collect::<Vec<_>>().join(""),
        x,
        y,
        width: line.width,
        available_width,
        line_height: line.line_height,
        runs,
        is_justify,
        id,
        page_reference,
    }
}

fn layout_run(run: &InlineRun, x_offset: f32) -> LayoutRun {
    LayoutRun {
        text: run.text.clone(),
        x_offset,
        y_offset: 0.0,
        width: run.width,
        font_size: run.style.font_size,
        style: run.style.clone(),
        is_justify: false,
        available_width: 0.0,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::model::{Style, TableStyle};
    use crate::measure::{MeasuredTable, MeasuredTableRow, MeasuredTableCell};
    use crate::text::{InlineLine, InlineRun};
    use crate::style::ResolvedStyle;

    fn make_style() -> ResolvedStyle {
        ResolvedStyle::from(&Style::new())
    }

    fn make_cell(text: &str, width: f32, height: f32) -> MeasuredTableCell {
        let style = make_style();
        MeasuredTableCell {
            lines: vec![InlineLine {
                runs: vec![InlineRun { text: text.to_string(), width: 50.0, style: style.clone() }],
                width: 50.0,
                line_height: 14.0,
            }],
            items: vec![],
            width,
            height,
            padding: 2.0,
            style,
            col_span: 1,
            row_span: 1,
            vertical_alignment: None,
            border: None,
        }
    }

    fn make_table(rows: usize, h_line_when_broken: Option<bool>) -> MeasuredTable {
        let row_height = 20.0;
        let col_width = 100.0;
        MeasuredTable {
            column_widths: vec![col_width],
            rows: (0..rows)
                .map(|i| MeasuredTableRow {
                    cells: vec![make_cell(&format!("R{}", i), col_width, row_height)],
                    height: row_height,
                })
                .collect(),
            style: make_style(),
            height: rows as f32 * row_height,
            header_rows: None,
            dont_break_rows: false,
            page_break: None,
            heights: None,
            keep_with_header_rows: None,
            table_style: None,
            layout_config: None,
            h_line_when_broken,
            origin_node_idx: None,
        }
    }

    fn horizontal_border_at(table: &LayoutTable, y: f32) -> bool {
        table.borders.iter().any(|l| (l.y1 - y).abs() < 0.01 && (l.y2 - y).abs() < 0.01)
    }

    fn horizontal_cell_border_at(table: &LayoutTable, y: f32) -> bool {
        table.rows.iter().any(|row| {
            row.cells.iter().any(|cell| {
                cell.borders.iter().any(|l| (l.y1 - y).abs() < 0.01 && (l.y2 - y).abs() < 0.01)
            })
        })
    }

    #[test]
    fn first_fragment_removes_bottom_border_at_break() {
        let table = make_table(3, Some(false));
        let result = layout_table_with_break(&table, 0.0, 0.0, true, false);
        // first_row_y = 0.0, last_row_y = 60.0 (3 rows * 20.0)
        // First fragment: should keep top (y=0) and remove bottom (y=60)
        assert!(horizontal_border_at(&result, 0.0), "first fragment should keep top border");
        assert!(!horizontal_border_at(&result, 60.0), "first fragment should remove bottom border at break");
        assert!(horizontal_cell_border_at(&result, 0.0), "first fragment should keep top cell border");
        assert!(!horizontal_cell_border_at(&result, 60.0), "first fragment should remove bottom cell border at break");
    }

    #[test]
    fn last_fragment_removes_top_border_at_break() {
        let table = make_table(3, Some(false));
        let result = layout_table_with_break(&table, 0.0, 0.0, false, true);
        // first_row_y = 0.0, last_row_y = 60.0
        // Last fragment: should remove top (y=0) at break and keep bottom (y=60)
        assert!(!horizontal_border_at(&result, 0.0), "last fragment should remove top border at break");
        assert!(horizontal_border_at(&result, 60.0), "last fragment should keep bottom border");
        assert!(!horizontal_cell_border_at(&result, 0.0), "last fragment should remove top cell border at break");
        assert!(horizontal_cell_border_at(&result, 60.0), "last fragment should keep bottom cell border");
    }

    #[test]
    fn middle_fragment_removes_both_borders_at_break() {
        let table = make_table(3, Some(false));
        let result = layout_table_with_break(&table, 0.0, 0.0, false, false);
        // Middle fragment: should remove both top (y=0) and bottom (y=60) at break points
        assert!(!horizontal_border_at(&result, 0.0), "middle fragment should remove top border at break");
        assert!(!horizontal_border_at(&result, 60.0), "middle fragment should remove bottom border at break");
        assert!(!horizontal_cell_border_at(&result, 0.0), "middle fragment should remove top cell border at break");
        assert!(!horizontal_cell_border_at(&result, 60.0), "middle fragment should remove bottom cell border at break");
    }

    #[test]
    fn single_fragment_keeps_all_borders() {
        let table = make_table(3, Some(false));
        // is_first=true, is_last=true → single fragment, no breaks to filter
        let result = layout_table_with_break(&table, 0.0, 0.0, true, true);
        assert!(horizontal_border_at(&result, 0.0), "single fragment should keep top border");
        assert!(horizontal_border_at(&result, 60.0), "single fragment should keep bottom border");
    }

    #[test]
    fn h_line_when_broken_true_preserves_all_borders() {
        let table = make_table(3, Some(true));
        // Middle fragment with h_line_when_broken=true should keep all borders
        let result = layout_table_with_break(&table, 0.0, 0.0, false, false);
        assert!(horizontal_border_at(&result, 0.0), "h_line_when_broken=true should keep top border");
        assert!(horizontal_border_at(&result, 60.0), "h_line_when_broken=true should keep bottom border");
    }

    #[test]
    fn h_line_when_broken_none_preserves_all_borders() {
        let table = make_table(3, None);
        let result = layout_table_with_break(&table, 0.0, 0.0, false, false);
        assert!(horizontal_border_at(&result, 0.0), "h_line_when_broken=None should keep top border");
        assert!(horizontal_border_at(&result, 60.0), "h_line_when_broken=None should keep bottom border");
    }

    #[test]
    fn border_side_identifies_all_sides() {
        let (x, y, w, h) = (10.0, 20.0, 100.0, 50.0);
        let borders = cell_borders(x, y, w, h);

        assert_eq!(border_side(&borders[0], x, y, w, h), Some(0), "top");
        assert_eq!(border_side(&borders[1], x, y, w, h), Some(1), "right");
        assert_eq!(border_side(&borders[2], x, y, w, h), Some(2), "bottom");
        assert_eq!(border_side(&borders[3], x, y, w, h), Some(3), "left");
    }

    #[test]
    fn filter_cell_borders_with_table_style_light_horizontal_keeps_correct_sides() {
        let (x, y, w, h) = (0.0, 0.0, 200.0, 100.0);
        let raw = cell_borders(x, y, w, h);
        let filtered = filter_borders(&raw, Some(TableStyle::LightHorizontalLines), None);
        assert_eq!(filtered.len(), 2, "should have top and bottom");

        // Disable top (border[1]), keep bottom (border[3])
        let border = Some(vec![false, false, false, true]); // [left, top, right, bottom]
        let result = filter_cell_borders(&filtered, &border, x, y, w, h);
        assert_eq!(result.len(), 1, "should have only bottom");
        assert!((result[0].y1 - (y + h)).abs() < 0.5, "remaining border should be bottom");
    }

    #[test]
    fn filter_cell_borders_with_table_style_light_vertical_keeps_correct_sides() {
        let (x, y, w, h) = (0.0, 0.0, 200.0, 100.0);
        let raw = cell_borders(x, y, w, h);
        let filtered = filter_borders(&raw, Some(TableStyle::LightVerticalLines), None);
        assert_eq!(filtered.len(), 2, "should have right and left");

        // Disable right (border[2]), keep left (border[0])
        let border = Some(vec![true, false, false, false]); // [left, top, right, bottom]
        let result = filter_cell_borders(&filtered, &border, x, y, w, h);
        assert_eq!(result.len(), 1, "should have only left");
        assert!((result[0].x1 - x).abs() < 0.5, "remaining border should be left");
    }

    #[test]
    fn filter_cell_borders_without_table_style_uses_correct_indices() {
        let (x, y, w, h) = (0.0, 0.0, 200.0, 100.0);
        let raw = cell_borders(x, y, w, h);
        let filtered = filter_borders(&raw, None, None);
        assert_eq!(filtered.len(), 4);

        // Disable top and bottom, keep left and right
        let border = Some(vec![true, false, true, false]); // [left, top, right, bottom]
        let result = filter_cell_borders(&filtered, &border, x, y, w, h);
        assert_eq!(result.len(), 2, "should have left and right");
        for line in &result {
            let side = border_side(line, x, y, w, h);
            assert!(side == Some(1) || side == Some(3), "should be right or left, got {:?}", side);
        }
    }

    #[test]
    fn filter_cell_borders_none_border_keeps_all() {
        let (x, y, w, h) = (0.0, 0.0, 200.0, 100.0);
        let raw = cell_borders(x, y, w, h);
        let filtered = filter_borders(&raw, Some(TableStyle::CrossLines), None);
        let result = filter_cell_borders(&filtered, &None, x, y, w, h);
        assert_eq!(result.len(), 4, "None border should keep all");
    }

    #[test]
    fn auto_watermark_font_size_returns_reasonable_size() {
        let ps = PageSize { width: 595.0, height: 842.0, auto_height: false };
        let measure = |fs: f32| crate::text::measure_helvetica("CONFIDENTIAL", fs);
        let size = watermark::auto_watermark_font_size_with("CONFIDENTIAL", -45.0, &ps, &measure);
        assert!(size > 0.0, "font size should be positive");
        assert!(size < 1000.0, "font size should be under 1000");
    }

    #[test]
    fn auto_watermark_font_size_empty_text_returns_default() {
        let ps = PageSize { width: 595.0, height: 842.0, auto_height: false };
        let measure = |fs: f32| crate::text::measure_helvetica("", fs);
        let size = watermark::auto_watermark_font_size_with("", -45.0, &ps, &measure);
        assert_eq!(size, 10.0, "empty text should return default 10.0");
    }

    #[test]
    fn auto_watermark_font_size_scales_with_text_length() {
        let ps = PageSize { width: 595.0, height: 842.0, auto_height: false };
        let measure_short = |fs: f32| crate::text::measure_helvetica("AB", fs);
        let measure_long = |fs: f32| crate::text::measure_helvetica("ABCDEFGHIJ", fs);
        let short = watermark::auto_watermark_font_size_with("AB", -45.0, &ps, &measure_short);
        let long = watermark::auto_watermark_font_size_with("ABCDEFGHIJ", -45.0, &ps, &measure_long);
        assert!(short > long, "shorter text should get larger font size");
    }

    #[test]
    fn auto_watermark_font_size_respects_angle() {
        let ps = PageSize { width: 595.0, height: 842.0, auto_height: false };
        let measure = |fs: f32| crate::text::measure_helvetica("TEST", fs);
        let size_45 = watermark::auto_watermark_font_size_with("TEST", -45.0, &ps, &measure);
        let size_0 = watermark::auto_watermark_font_size_with("TEST", 0.0, &ps, &measure);
        assert!(size_45 > size_0, "angled watermark should allow larger font than horizontal");
    }

    #[test]
    fn watermark_fixed_font_size_preserved() {
        use crate::model::{WatermarkConfig, WatermarkFontSize};
        let wm = WatermarkConfig::new("TEST").font_size(42.0);
        assert_eq!(wm.font_size, Some(WatermarkFontSize::Fixed(42.0)));
    }

    #[test]
    fn watermark_auto_font_size_builder() {
        use crate::model::{WatermarkConfig, WatermarkFontSize};
        let wm = WatermarkConfig::new("TEST").auto_font_size();
        assert_eq!(wm.font_size, Some(WatermarkFontSize::Auto));
    }

    #[test]
    fn watermark_default_font_size_is_none() {
        use crate::model::WatermarkConfig;
        let wm = WatermarkConfig::new("TEST");
        assert_eq!(wm.font_size, None, "default should be None (auto)");
    }
}
