use pdfmake_rust::{Alignment, DecorationStyle, Document, DocumentNode, InlineFragment, Style, TextDecoration, TextNode};

#[test]
fn text_node_plain_text_creates_single_inline_fragment() {
    let node = TextNode::new("Hello");
    assert_eq!(node.inlines.len(), 1);
    assert_eq!(node.inlines[0].text, "Hello");
}

#[test]
fn text_node_accepts_mixed_inline_fragments() {
    let node = TextNode::with_inlines(vec![
        InlineFragment::new("normal "),
        InlineFragment::new("bold").style(Style::new().bold(true)),
    ]);

    assert_eq!(node.inlines.len(), 2);
    assert_eq!(node.inlines[1].style.bold, Some(true));
}

#[test]
fn style_supports_alignment_and_decoration() {
    let style = Style::new()
        .alignment(Alignment::Center)
        .decoration(TextDecoration::Underline);

    assert_eq!(style.alignment, Some(Alignment::Center));
    assert_eq!(style.decoration, Some(TextDecoration::Underline));
}

use pdfmake_rust::style::ResolvedStyle;
use pdfmake_rust::{InlineLine, NormalizedInline, TextMeasurer, wrap_inlines};

fn resolved(font_size: f32, bold: bool) -> ResolvedStyle {
    ResolvedStyle {
        font_size,
        bold,
        italics: false,
        font_family: None,
        line_height: 1.0,
        alignment: Alignment::Left,
        decoration: None,
        color: None,
        background: None,
        margin: None,
        fill_color: None,
        fill_opacity: None,
        opacity: None,
        decoration_style: None,
        decoration_color: None,
        decoration_thickness: None,
        link: None,
        border_color: None,
        character_spacing: None,
        no_wrap: false,
        unbreakable: false,
        sup: false,
        sub: false,
        word_break: None,
        leading_indent: 0.0,
        preserve_leading_spaces: false,
        preserve_trailing_spaces: false,
        font_features: None,
        overlay_pattern: None,
        overlay_opacity: None,
    }
}

#[test]
fn text_measurer_caches_repeated_measurements_with_hash_safe_key() {
    let mut measurer = TextMeasurer::new();
    let first = measurer.measure("test", 12.0);
    let second = measurer.measure("test", 12.0);
    assert_eq!(first, second);
}

#[test]
fn wrap_inlines_preserves_styles_across_words() {
    let mut measurer = TextMeasurer::new();
    let fragments = vec![
        NormalizedInline { text: "hello ".into(), style: resolved(10.0, false) },
        NormalizedInline { text: "bold world".into(), style: resolved(10.0, true) },
    ];

    let lines = wrap_inlines(&fragments, 500.0, &mut measurer);

    assert_eq!(lines.len(), 1);
    assert_eq!(lines[0].runs.len(), 2);
    assert_eq!(lines[0].runs[1].style.bold, true);
}

#[test]
fn wrap_inlines_splits_long_single_fragment_by_words() {
    let mut measurer = TextMeasurer::new();
    let fragments = vec![NormalizedInline {
        text: "one two three four five six".into(),
        style: resolved(20.0, false),
    }];

    let lines = wrap_inlines(&fragments, 70.0, &mut measurer);

    assert!(lines.len() > 1, "expected multiple lines, got {}", lines.len());
    assert!(lines.iter().all(|line| line.width <= 70.0 || line.runs.len() == 1));
}

use pdfmake_rust::normalize_document;
use pdfmake_rust::NormalizedBlock;

#[test]
fn normalize_resolves_styles_per_inline_fragment() {
    let document = Document::builder()
        .default_style(Style::default().font_size(10.0))
        .content(DocumentNode::Text(TextNode::with_inlines(vec![
            InlineFragment::new("normal "),
            InlineFragment::new("bold").style(Style::new().bold(true).font_size(16.0)),
        ])))
        .build();

    let normalized = normalize_document(&document).unwrap();

    let block = match &normalized.blocks[0] {
        NormalizedBlock::Text(block) => block,
        NormalizedBlock::Table(_) => panic!("expected text block"),
        _ => panic!("expected text block"),
    };
    assert_eq!(block.inlines.len(), 2);
    assert_eq!(block.inlines[0].style.font_size, 10.0);
    assert_eq!(block.inlines[1].style.font_size, 16.0);
    assert_eq!(block.inlines[1].style.bold, true);
}

#[test]
fn normalize_cascade_order_is_default_named_block_then_fragment() {
    let document = Document::builder()
        .default_style(Style::default().font_size(8.0).alignment(Alignment::Left))
        .style("large", Style::new().font_size(12.0))
        .content(DocumentNode::Text(
            TextNode::with_inlines(vec![
                InlineFragment::new("center bold").style(Style::new().bold(true)),
            ])
            .style("large")
            .inline_style(Style::new().alignment(Alignment::Center)),
        ))
        .build();

    let normalized = normalize_document(&document).unwrap();
    let block = match &normalized.blocks[0] {
        NormalizedBlock::Text(block) => block,
        NormalizedBlock::Table(_) => panic!("expected text block"),
        _ => panic!("expected text block"),
    };
    let inline = &block.inlines[0];

    assert_eq!(inline.style.font_size, 12.0);
    assert_eq!(inline.style.alignment, Alignment::Center);
    assert_eq!(inline.style.bold, true);
}

use pdfmake_rust::{measure_document, FontRegistry, MeasuredBlock};

#[test]
fn measure_document_outputs_inline_lines_with_run_widths() {
    let document = Document::builder()
        .content(DocumentNode::Text(TextNode::with_inlines(vec![
            InlineFragment::new("plain "),
            InlineFragment::new("bold").style(Style::new().bold(true)),
        ])))
        .build();

    let normalized = normalize_document(&document).unwrap();
    let measured = measure_document(&normalized, &FontRegistry::empty()).unwrap();

    assert_eq!(measured.blocks.len(), 1);
    let MeasuredBlock::Text(text_block) = &measured.blocks[0] else { panic!("expected text block") };
    assert!(!text_block.lines.is_empty());
    assert!(text_block.lines[0].width > 0.0);
    assert!(text_block.lines[0].runs.iter().any(|run| run.style.bold));
}

use pdfmake_rust::{layout_document, Margins, PageSize, LayoutItem};

#[test]
fn layout_preserves_positioned_runs_for_mixed_styles() {
    let document = Document::builder()
        .content(DocumentNode::Text(TextNode::with_inlines(vec![
            InlineFragment::new("plain "),
            InlineFragment::new("bold").style(Style::new().bold(true)),
        ])))
        .build();

    let normalized = normalize_document(&document).unwrap();
    let measured = measure_document(&normalized, &FontRegistry::empty()).unwrap();
    let layout = layout_document(&measured).unwrap();
    let item = match &layout.pages[0].items[0] {
        LayoutItem::Text(item) => item,
        LayoutItem::Table(_) => panic!("expected text item"),
        _ => panic!("expected text item"),
    };

    assert_eq!(item.runs.len(), 2);
    assert_eq!(item.runs[0].text, "plain ");
    assert_eq!(item.runs[1].text, "bold");
    assert_eq!(item.runs[1].style.bold, true);
    assert!(item.runs[1].x_offset > item.runs[0].x_offset);
}

#[test]
fn layout_applies_center_and_right_alignment_to_whole_line() {
    let page_width = 400.0;
    let margin = 20.0;
    let document = Document::builder()
        .page_size(PageSize::new(page_width, 200.0))
        .page_margins(Margins::all(margin))
        .content(DocumentNode::Text(TextNode::with_inlines(vec![
            InlineFragment::new("center").style(Style::new().alignment(Alignment::Center)),
        ])))
        .build();

    let normalized = normalize_document(&document).unwrap();
    let measured = measure_document(&normalized, &FontRegistry::empty()).unwrap();
    let layout = layout_document(&measured).unwrap();
    let item = match &layout.pages[0].items[0] {
        LayoutItem::Text(item) => item,
        LayoutItem::Table(_) => panic!("expected text item"),
        _ => panic!("expected text item"),
    };
    let available = page_width - margin * 2.0;
    let expected = margin + (available - item.width) / 2.0;

    assert!((item.x - expected).abs() < 1.0);
}

use pdfmake_rust::PdfMake;

#[test]
fn render_uses_bold_font_for_bold_run_only() {
    let document = Document::builder()
        .content(DocumentNode::Text(TextNode::with_inlines(vec![
            InlineFragment::new("plain "),
            InlineFragment::new("bold").style(Style::new().bold(true)),
        ])))
        .build();

    let bytes = PdfMake::new().render(&document).unwrap();
    let pdf = String::from_utf8_lossy(&bytes);

    assert!(pdf.contains("/BaseFont /Helvetica-Bold"));
    assert!(pdf.contains("/F1"));
    assert!(pdf.contains("/F2"));
}

#[test]
fn render_uses_bold_oblique_for_bold_italic_run() {
    let document = Document::builder()
        .content(DocumentNode::Text(TextNode::with_inlines(vec![
            InlineFragment::new("both").style(Style::new().bold(true).italics(true)),
        ])))
        .build();

    let bytes = PdfMake::new().render(&document).unwrap();
    let pdf = String::from_utf8_lossy(&bytes);

    assert!(pdf.contains("/BaseFont /Helvetica-BoldOblique"));
    assert!(pdf.contains("/F4"));
}

#[test]
fn render_draws_decoration_for_decorated_run() {
    let document = Document::builder()
        .content(DocumentNode::Text(TextNode::with_inlines(vec![
            InlineFragment::new("plain "),
            InlineFragment::new("underlined").style(Style::new().decoration(TextDecoration::Underline)),
        ])))
        .build();

    let bytes = PdfMake::new().render(&document).unwrap();
    let pdf = String::from_utf8_lossy(&bytes);

    assert!(pdf.contains("/Filter /FlateDecode"));
    assert!(pdf.contains("stream\n"));
    assert!(pdf.contains("endstream"));
}

fn decompress_pdf_streams(pdf_bytes: &[u8]) -> Vec<String> {
    use flate2::read::ZlibDecoder;
    use std::io::Read;

    let mut results = Vec::new();
    let pdf_str = pdf_bytes;
    let mut pos = 0;
    while let Some(start) = find_subsequence(&pdf_str[pos..], b"stream\n") {
        let stream_start = pos + start + 7;
        if let Some(end) = find_subsequence(&pdf_str[stream_start..], b"\nendstream") {
            let compressed = &pdf_str[stream_start..stream_start + end];
            let mut decoder = ZlibDecoder::new(compressed);
            let mut decompressed = String::new();
            if decoder.read_to_string(&mut decompressed).is_ok() {
                results.push(decompressed);
            }
            pos = stream_start + end;
        } else {
            break;
        }
    }
    results
}

fn find_subsequence(haystack: &[u8], needle: &[u8]) -> Option<usize> {
    haystack.windows(needle.len()).position(|w| w == needle)
}

#[test]
fn text_with_background_emits_rect_before_text() {
    let document = Document::builder()
        .content(DocumentNode::Text(TextNode::new("Highlighted").inline_style(Style::new().background("#FFFF00"))))
        .build();

    let bytes = PdfMake::new().render(&document).unwrap();
    let streams = decompress_pdf_streams(&bytes);

    let page_stream = streams
        .iter()
        .find(|s| s.contains("BT"))
        .expect("expected a page content stream with BT");

    let rect_pos = page_stream.find("re f").expect("expected filled rectangle (re f) in stream");
    let bt_pos = page_stream.find("BT").expect("expected BT in stream");
    assert!(rect_pos < bt_pos, "background rect must appear before text BT");

    assert!(page_stream.contains("q "));
    assert!(page_stream.contains(" Q\n"));
}

#[test]
fn text_without_background_has_no_rect() {
    let document = Document::builder()
        .content(DocumentNode::text("Plain"))
        .build();

    let bytes = PdfMake::new().render(&document).unwrap();
    let streams = decompress_pdf_streams(&bytes);

    let page_stream = streams
        .iter()
        .find(|s| s.contains("BT"))
        .expect("expected a page content stream with BT");

    assert!(!page_stream.contains("re f"));
}

#[test]
fn decoration_dashed_renders_multiple_segments() {
    let doc = Document::builder()
        .content(DocumentNode::Text(TextNode::new("underlined").inline_style(
            Style::new().decoration(TextDecoration::Underline).decoration_style(DecorationStyle::Dashed)
        )))
        .build();

    let bytes = PdfMake::new().render(&doc).unwrap();
    let streams = decompress_pdf_streams(&bytes);

    let page_stream = streams
        .iter()
        .find(|s| s.contains("BT"))
        .expect("expected a page content stream with BT");

    let segment_count = page_stream.matches(" m ").count();
    assert!(segment_count > 1, "dashed decoration should produce multiple line segments, got {}", segment_count);
}

#[test]
fn decoration_with_color_emits_rg_instruction() {
    let doc = Document::builder()
        .content(DocumentNode::Text(TextNode::new("red underline").inline_style(
            Style::new().decoration(TextDecoration::Underline).decoration_color("#FF0000")
        )))
        .build();

    let bytes = PdfMake::new().render(&doc).unwrap();
    let streams = decompress_pdf_streams(&bytes);

    let page_stream = streams
        .iter()
        .find(|s| s.contains("BT"))
        .expect("expected a page content stream with BT");

    assert!(page_stream.contains("1.000 0.000 0.000 RG"), "expected red stroke color RG instruction");
    assert!(page_stream.contains("0 0 0 RG"), "expected stroke color reset to black");
}

#[test]
fn decoration_wavy_renders_bezier_curves() {
    let doc = Document::builder()
        .content(DocumentNode::Text(TextNode::new("wavy").inline_style(
            Style::new().decoration(TextDecoration::Underline).decoration_style(DecorationStyle::Wavy)
        )))
        .build();

    let bytes = PdfMake::new().render(&doc).unwrap();
    let streams = decompress_pdf_streams(&bytes);

    let page_stream = streams
        .iter()
        .find(|s| s.contains("BT"))
        .expect("expected a page content stream with BT");

    assert!(page_stream.contains(" c\n"), "wavy decoration should use bezier curves (c operator)");
}

#[test]
fn decoration_solid_default_produces_single_line() {
    let doc = Document::builder()
        .content(DocumentNode::Text(TextNode::new("underline").inline_style(
            Style::new().decoration(TextDecoration::Underline)
        )))
        .build();

    let bytes = PdfMake::new().render(&doc).unwrap();
    let streams = decompress_pdf_streams(&bytes);

    let page_stream = streams
        .iter()
        .find(|s| s.contains("BT"))
        .expect("expected a page content stream with BT");

    let segment_count = page_stream.matches(" m ").count();
    assert_eq!(segment_count, 1, "solid decoration should produce exactly one line segment, got {}", segment_count);
}

#[test]
fn alignment_enum_has_justify_variant() {
    let style = Style::new().alignment(Alignment::Justify);
    assert_eq!(style.alignment, Some(Alignment::Justify));
}

#[test]
fn justify_text_renders_without_error() {
    let doc = Document::builder()
        .content(DocumentNode::Text(
            TextNode::new("This is a longer piece of text that should be justified across the available width when rendered in the PDF document.")
                .inline_style(Style::new().alignment(Alignment::Justify))
        ))
        .build();
    let pdf = PdfMake::new().render(&doc).unwrap();
    assert!(pdf.len() > 100);
}

#[test]
fn style_accepts_no_wrap() {
    let style = Style::new().no_wrap(true);
    assert_eq!(style.no_wrap, Some(true));
}

#[test]
fn no_wrap_text_stays_single_line() {
    use pdfmake_rust::{normalize_document, measure_document, MeasuredBlock, FontRegistry};
    let doc = Document::builder()
        .content(DocumentNode::Text(
            TextNode::new("This is a very long text that should not wrap to multiple lines even if it exceeds the available width of the page")
                .inline_style(Style::new().no_wrap(true).font_size(12.0))
        ))
        .build();
    let normalized = normalize_document(&doc).unwrap();
    let fonts = FontRegistry::empty();
    let measured = measure_document(&normalized, &fonts).unwrap();
    match &measured.blocks[0] {
        MeasuredBlock::Text(t) => {
            assert_eq!(t.lines.len(), 1, "noWrap text should stay on single line");
        }
        _ => panic!("expected text block"),
    }
}

#[test]
fn style_accepts_character_spacing() {
    let style = Style::new().character_spacing(2.0);
    assert_eq!(style.character_spacing, Some(2.0));
}

#[test]
fn character_spacing_renders() {
    let doc = Document::builder()
        .content(DocumentNode::Text(
            TextNode::new("Spaced out text").inline_style(Style::new().character_spacing(3.0))
        ))
        .build();
    let pdf = PdfMake::new().render(&doc).unwrap();
    assert!(pdf.len() > 100);
}

#[test]
fn multiline_justify_table_cell_renders_without_crash() {
    use pdfmake_rust::{TableNode, TableCell};
    let doc = Document::builder()
        .content(DocumentNode::Table(
            TableNode::new(vec![vec![
                TableCell::text("This is a long text that should wrap across multiple lines inside a narrow table cell with justify alignment enabled so that word spacing is computed per line rather than across the entire cell content.")
                    .style(Style::new().alignment(Alignment::Justify))
            ]])
        ))
        .build();
    let pdf = PdfMake::new().render(&doc).unwrap();
    assert!(pdf.len() > 100);
}

#[test]
fn justify_in_table_cell_renders() {
    use pdfmake_rust::{TableNode, TableCell};
    let doc = Document::builder()
        .content(DocumentNode::Table(
            TableNode::new(vec![vec![
                TableCell::text("This is justified text inside a table cell that should spread across the available width.")
                    .style(Style::new().alignment(Alignment::Justify))
            ]])
        ))
        .build();
    let pdf = PdfMake::new().render(&doc).unwrap();
    assert!(pdf.len() > 100);
}