Skip to main content

sheetkit_core/
stream.rs

1//! Streaming worksheet writer.
2//!
3//! The [`StreamWriter`] builds `WorksheetXml` structs directly instead of
4//! writing raw XML text. Rows must be written in ascending order.
5//!
6//! # Example
7//!
8//! ```
9//! use sheetkit_core::stream::StreamWriter;
10//! use sheetkit_core::cell::CellValue;
11//!
12//! let mut sw = StreamWriter::new("Sheet1");
13//! sw.set_col_width(1, 20.0).unwrap();
14//! sw.write_row(1, &[CellValue::from("Name"), CellValue::from("Age")]).unwrap();
15//! sw.write_row(2, &[CellValue::from("Alice"), CellValue::from(30)]).unwrap();
16//! let xml_bytes = sw.finish().unwrap();
17//! assert!(!xml_bytes.is_empty());
18//! ```
19
20use crate::cell::CellValue;
21use crate::error::{Error, Result};
22use crate::sst::SharedStringTable;
23use crate::utils::cell_ref::cell_name_to_coordinates;
24use crate::utils::constants::{MAX_COLUMNS, MAX_ROWS, MAX_ROW_HEIGHT};
25
26use sheetkit_xml::worksheet::{
27    Cell, CellFormula, CellTypeTag, Col, Cols, CompactCellRef, MergeCell, MergeCells, Pane, Row,
28    SheetData, SheetView, SheetViews, WorksheetXml,
29};
30
31/// XML declaration prepended to the worksheet XML.
32const XML_DECLARATION: &str = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>"#;
33
34/// Maximum outline level allowed by Excel.
35const MAX_OUTLINE_LEVEL: u8 = 7;
36
37/// Options for a streamed row.
38#[derive(Debug, Clone, Default)]
39pub struct StreamRowOptions {
40    /// Custom row height in points.
41    pub height: Option<f64>,
42    /// Row visibility (false = hidden).
43    pub visible: Option<bool>,
44    /// Outline level (0-7).
45    pub outline_level: Option<u8>,
46    /// Style ID for the row.
47    pub style_id: Option<u32>,
48}
49
50/// Internal column configuration beyond width.
51#[derive(Debug, Clone, Default)]
52struct StreamColOptions {
53    width: Option<f64>,
54    style_id: Option<u32>,
55    hidden: Option<bool>,
56    outline_level: Option<u8>,
57}
58
59/// A streaming worksheet writer that writes rows sequentially.
60///
61/// Rows must be written in ascending order. The StreamWriter builds
62/// `Row` structs directly, then assembles them into a `WorksheetXml`
63/// on finish.
64#[derive(Debug)]
65pub struct StreamWriter {
66    sheet_name: String,
67    rows: Vec<Row>,
68    last_row: u32,
69    started: bool,
70    finished: bool,
71    col_widths: Vec<(u32, u32, f64)>,
72    col_options: Vec<(u32, StreamColOptions)>,
73    sst: SharedStringTable,
74    merge_cells: Vec<String>,
75    /// Freeze pane: (x_split, y_split, top_left_cell).
76    freeze_pane: Option<(u32, u32, String)>,
77}
78
79impl StreamWriter {
80    /// Create a new StreamWriter for the given sheet name.
81    pub fn new(sheet_name: &str) -> Self {
82        Self {
83            sheet_name: sheet_name.to_string(),
84            rows: Vec::new(),
85            last_row: 0,
86            started: false,
87            finished: false,
88            col_widths: Vec::new(),
89            col_options: Vec::new(),
90            sst: SharedStringTable::new(),
91            merge_cells: Vec::new(),
92            freeze_pane: None,
93        }
94    }
95
96    /// Get the sheet name.
97    pub fn sheet_name(&self) -> &str {
98        &self.sheet_name
99    }
100
101    /// Set column width for a single column (1-based).
102    /// Must be called before any write_row() calls.
103    pub fn set_col_width(&mut self, col: u32, width: f64) -> Result<()> {
104        self.set_col_width_range(col, col, width)
105    }
106
107    /// Set column width for a range (min_col..=max_col), both 1-based.
108    /// Must be called before any write_row() calls.
109    pub fn set_col_width_range(&mut self, min_col: u32, max_col: u32, width: f64) -> Result<()> {
110        if self.finished {
111            return Err(Error::StreamAlreadyFinished);
112        }
113        if self.started {
114            return Err(Error::StreamColumnsAfterRows);
115        }
116        if min_col == 0 || max_col == 0 || min_col > MAX_COLUMNS || max_col > MAX_COLUMNS {
117            return Err(Error::InvalidColumnNumber(if min_col == 0 {
118                min_col
119            } else {
120                max_col
121            }));
122        }
123        self.col_widths.push((min_col, max_col, width));
124        Ok(())
125    }
126
127    /// Set column style for a single column (1-based).
128    /// Must be called before any write_row() calls.
129    pub fn set_col_style(&mut self, col: u32, style_id: u32) -> Result<()> {
130        self.ensure_col_configurable(col)?;
131        self.get_or_create_col_options(col).style_id = Some(style_id);
132        Ok(())
133    }
134
135    /// Set column visibility for a single column (1-based).
136    /// Must be called before any write_row() calls.
137    pub fn set_col_visible(&mut self, col: u32, visible: bool) -> Result<()> {
138        self.ensure_col_configurable(col)?;
139        self.get_or_create_col_options(col).hidden = Some(!visible);
140        Ok(())
141    }
142
143    /// Set column outline level for a single column (1-based).
144    /// Must be called before any write_row() calls. Level must be 0-7.
145    pub fn set_col_outline_level(&mut self, col: u32, level: u8) -> Result<()> {
146        self.ensure_col_configurable(col)?;
147        if level > MAX_OUTLINE_LEVEL {
148            return Err(Error::OutlineLevelExceeded {
149                level,
150                max: MAX_OUTLINE_LEVEL,
151            });
152        }
153        self.get_or_create_col_options(col).outline_level = Some(level);
154        Ok(())
155    }
156
157    /// Set freeze panes for the streamed sheet.
158    /// Must be called before any write_row() calls.
159    /// `top_left_cell` is the cell below and to the right of the frozen area,
160    /// e.g., "A2" freezes row 1, "B1" freezes column A, "C3" freezes rows 1-2
161    /// and columns A-B.
162    pub fn set_freeze_panes(&mut self, top_left_cell: &str) -> Result<()> {
163        if self.finished {
164            return Err(Error::StreamAlreadyFinished);
165        }
166        if self.started {
167            return Err(Error::StreamColumnsAfterRows);
168        }
169        let (col, row) = cell_name_to_coordinates(top_left_cell)?;
170        if col == 1 && row == 1 {
171            return Err(Error::InvalidCellReference(
172                "freeze pane at A1 has no effect".to_string(),
173            ));
174        }
175        self.freeze_pane = Some((col - 1, row - 1, top_left_cell.to_string()));
176        Ok(())
177    }
178
179    /// Write a row of values with row-level options.
180    pub fn write_row_with_options(
181        &mut self,
182        row: u32,
183        values: &[CellValue],
184        options: &StreamRowOptions,
185    ) -> Result<()> {
186        self.write_row_impl(row, values, None, Some(options))
187    }
188
189    /// Write a row of values. Rows must be written in ascending order.
190    /// Row numbers are 1-based.
191    pub fn write_row(&mut self, row: u32, values: &[CellValue]) -> Result<()> {
192        self.write_row_impl(row, values, None, None)
193    }
194
195    /// Write a row with a specific style ID applied to all cells.
196    pub fn write_row_with_style(
197        &mut self,
198        row: u32,
199        values: &[CellValue],
200        style_id: u32,
201    ) -> Result<()> {
202        self.write_row_impl(row, values, Some(style_id), None)
203    }
204
205    /// Add a merge cell reference (e.g., "A1:B2").
206    /// Must be called before finish(). The reference must be a valid range
207    /// in the form "A1:B2".
208    pub fn add_merge_cell(&mut self, reference: &str) -> Result<()> {
209        if self.finished {
210            return Err(Error::StreamAlreadyFinished);
211        }
212        // Validate the range format.
213        let parts: Vec<&str> = reference.split(':').collect();
214        if parts.len() != 2 {
215            return Err(Error::InvalidMergeCellReference(reference.to_string()));
216        }
217        cell_name_to_coordinates(parts[0])
218            .map_err(|_| Error::InvalidMergeCellReference(reference.to_string()))?;
219        cell_name_to_coordinates(parts[1])
220            .map_err(|_| Error::InvalidMergeCellReference(reference.to_string()))?;
221        self.merge_cells.push(reference.to_string());
222        Ok(())
223    }
224
225    /// Finish writing and return the complete worksheet XML bytes.
226    pub fn finish(&mut self) -> Result<Vec<u8>> {
227        if self.finished {
228            return Err(Error::StreamAlreadyFinished);
229        }
230        self.finished = true;
231        let ws = self.build_worksheet_xml();
232        let body = quick_xml::se::to_string(&ws).map_err(|e| Error::Internal(e.to_string()))?;
233        let mut xml = String::with_capacity(body.len() + 60);
234        xml.push_str(XML_DECLARATION);
235        xml.push('\n');
236        xml.push_str(&body);
237        Ok(xml.into_bytes())
238    }
239
240    /// Get a reference to the shared string table.
241    pub fn sst(&self) -> &SharedStringTable {
242        &self.sst
243    }
244
245    /// Consume the writer and return (xml_bytes, shared_string_table).
246    pub fn into_parts(mut self) -> Result<(Vec<u8>, SharedStringTable)> {
247        let xml = self.finish()?;
248        Ok((xml, self.sst))
249    }
250
251    /// Consume the writer and return the worksheet XML struct and shared string table.
252    /// This is the optimized path that avoids XML serialization/deserialization.
253    pub fn into_worksheet_parts(mut self) -> Result<(WorksheetXml, SharedStringTable)> {
254        if self.finished {
255            return Err(Error::StreamAlreadyFinished);
256        }
257        self.finished = true;
258        let ws = self.build_worksheet_xml_owned();
259        Ok((ws, self.sst))
260    }
261
262    /// Validate that column configuration is allowed (not finished, not started,
263    /// valid column number).
264    fn ensure_col_configurable(&self, col: u32) -> Result<()> {
265        if self.finished {
266            return Err(Error::StreamAlreadyFinished);
267        }
268        if self.started {
269            return Err(Error::StreamColumnsAfterRows);
270        }
271        if col == 0 || col > MAX_COLUMNS {
272            return Err(Error::InvalidColumnNumber(col));
273        }
274        Ok(())
275    }
276
277    /// Get or create a StreamColOptions entry for the given column.
278    fn get_or_create_col_options(&mut self, col: u32) -> &mut StreamColOptions {
279        if let Some(pos) = self.col_options.iter().position(|(c, _)| *c == col) {
280            &mut self.col_options[pos].1
281        } else {
282            self.col_options.push((col, StreamColOptions::default()));
283            let last = self.col_options.len() - 1;
284            &mut self.col_options[last].1
285        }
286    }
287
288    /// Build a `WorksheetXml` by cloning the accumulated rows.
289    fn build_worksheet_xml(&self) -> WorksheetXml {
290        self.build_worksheet_xml_inner(self.rows.clone())
291    }
292
293    /// Build a `WorksheetXml` by taking ownership of the accumulated rows.
294    fn build_worksheet_xml_owned(&mut self) -> WorksheetXml {
295        let rows = std::mem::take(&mut self.rows);
296        self.build_worksheet_xml_inner(rows)
297    }
298
299    /// Assemble the final `WorksheetXml` from the given rows and accumulated config.
300    fn build_worksheet_xml_inner(&self, rows: Vec<Row>) -> WorksheetXml {
301        let sheet_views = self
302            .freeze_pane
303            .as_ref()
304            .map(|(x_split, y_split, top_left_cell)| {
305                let active_pane = match (*x_split > 0, *y_split > 0) {
306                    (true, true) => "bottomRight",
307                    (true, false) => "topRight",
308                    (false, true) => "bottomLeft",
309                    (false, false) => unreachable!(),
310                };
311                SheetViews {
312                    sheet_views: vec![SheetView {
313                        tab_selected: Some(true),
314                        show_grid_lines: None,
315                        show_formulas: None,
316                        show_row_col_headers: None,
317                        zoom_scale: None,
318                        view: None,
319                        top_left_cell: None,
320                        workbook_view_id: 0,
321                        pane: Some(Pane {
322                            x_split: if *x_split > 0 { Some(*x_split) } else { None },
323                            y_split: if *y_split > 0 { Some(*y_split) } else { None },
324                            top_left_cell: Some(top_left_cell.clone()),
325                            active_pane: Some(active_pane.to_string()),
326                            state: Some("frozen".to_string()),
327                        }),
328                        selection: vec![],
329                    }],
330                }
331            });
332
333        let cols = {
334            let has_widths = !self.col_widths.is_empty();
335            let has_options = !self.col_options.is_empty();
336            if has_widths || has_options {
337                let mut col_defs = Vec::new();
338                for &(min, max, width) in &self.col_widths {
339                    col_defs.push(Col {
340                        min,
341                        max,
342                        width: Some(width),
343                        style: None,
344                        hidden: None,
345                        custom_width: Some(true),
346                        outline_level: None,
347                    });
348                }
349                for &(col_num, ref opts) in &self.col_options {
350                    col_defs.push(Col {
351                        min: col_num,
352                        max: col_num,
353                        width: opts.width,
354                        style: opts.style_id,
355                        hidden: opts.hidden,
356                        custom_width: if opts.width.is_some() {
357                            Some(true)
358                        } else {
359                            None
360                        },
361                        outline_level: opts.outline_level.filter(|&l| l > 0),
362                    });
363                }
364                Some(Cols { cols: col_defs })
365            } else {
366                None
367            }
368        };
369
370        let merge_cells = if self.merge_cells.is_empty() {
371            None
372        } else {
373            Some(MergeCells {
374                count: Some(self.merge_cells.len() as u32),
375                merge_cells: self
376                    .merge_cells
377                    .iter()
378                    .map(|r| MergeCell {
379                        reference: r.clone(),
380                    })
381                    .collect(),
382            })
383        };
384
385        WorksheetXml {
386            sheet_views,
387            cols,
388            sheet_data: SheetData { rows },
389            merge_cells,
390            ..WorksheetXml::default()
391        }
392    }
393
394    /// Internal unified implementation for writing a row.
395    fn write_row_impl(
396        &mut self,
397        row: u32,
398        values: &[CellValue],
399        cell_style_id: Option<u32>,
400        options: Option<&StreamRowOptions>,
401    ) -> Result<()> {
402        if self.finished {
403            return Err(Error::StreamAlreadyFinished);
404        }
405
406        // Validate row number.
407        if row == 0 || row > MAX_ROWS {
408            return Err(Error::InvalidRowNumber(row));
409        }
410
411        // Enforce ascending row order.
412        if row <= self.last_row {
413            return Err(Error::StreamRowAlreadyWritten { row });
414        }
415
416        // Validate column count.
417        if values.len() > MAX_COLUMNS as usize {
418            return Err(Error::InvalidColumnNumber(values.len() as u32));
419        }
420
421        // Validate row options if provided.
422        if let Some(opts) = options {
423            if let Some(height) = opts.height {
424                if height > MAX_ROW_HEIGHT {
425                    return Err(Error::RowHeightExceeded {
426                        height,
427                        max: MAX_ROW_HEIGHT,
428                    });
429                }
430            }
431            if let Some(level) = opts.outline_level {
432                if level > MAX_OUTLINE_LEVEL {
433                    return Err(Error::OutlineLevelExceeded {
434                        level,
435                        max: MAX_OUTLINE_LEVEL,
436                    });
437                }
438            }
439        }
440
441        self.started = true;
442        self.last_row = row;
443
444        let mut cells = Vec::new();
445
446        for (i, value) in values.iter().enumerate() {
447            let col = (i as u32) + 1;
448
449            match value {
450                CellValue::Empty => continue,
451                CellValue::String(s) => {
452                    let idx = self.sst.add(s);
453                    cells.push(Cell {
454                        r: CompactCellRef::from_coordinates(col, row),
455                        col,
456                        s: cell_style_id,
457                        t: CellTypeTag::SharedString,
458                        v: Some(idx.to_string()),
459                        f: None,
460                        is: None,
461                    });
462                }
463                CellValue::Number(n) => {
464                    cells.push(Cell {
465                        r: CompactCellRef::from_coordinates(col, row),
466                        col,
467                        s: cell_style_id,
468                        t: CellTypeTag::None,
469                        v: Some(n.to_string()),
470                        f: None,
471                        is: None,
472                    });
473                }
474                CellValue::Date(serial) => {
475                    cells.push(Cell {
476                        r: CompactCellRef::from_coordinates(col, row),
477                        col,
478                        s: cell_style_id,
479                        t: CellTypeTag::None,
480                        v: Some(serial.to_string()),
481                        f: None,
482                        is: None,
483                    });
484                }
485                CellValue::Bool(b) => {
486                    let val = if *b { "1" } else { "0" };
487                    cells.push(Cell {
488                        r: CompactCellRef::from_coordinates(col, row),
489                        col,
490                        s: cell_style_id,
491                        t: CellTypeTag::Boolean,
492                        v: Some(val.to_string()),
493                        f: None,
494                        is: None,
495                    });
496                }
497                CellValue::Formula { expr, .. } => {
498                    cells.push(Cell {
499                        r: CompactCellRef::from_coordinates(col, row),
500                        col,
501                        s: cell_style_id,
502                        t: CellTypeTag::None,
503                        v: None,
504                        f: Some(Box::new(CellFormula {
505                            t: None,
506                            reference: None,
507                            si: None,
508                            value: Some(expr.clone()),
509                        })),
510                        is: None,
511                    });
512                }
513                CellValue::Error(e) => {
514                    cells.push(Cell {
515                        r: CompactCellRef::from_coordinates(col, row),
516                        col,
517                        s: cell_style_id,
518                        t: CellTypeTag::Error,
519                        v: Some(e.clone()),
520                        f: None,
521                        is: None,
522                    });
523                }
524                CellValue::RichString(runs) => {
525                    let plain = crate::rich_text::rich_text_to_plain(runs);
526                    let idx = self.sst.add(&plain);
527                    cells.push(Cell {
528                        r: CompactCellRef::from_coordinates(col, row),
529                        col,
530                        s: cell_style_id,
531                        t: CellTypeTag::SharedString,
532                        v: Some(idx.to_string()),
533                        f: None,
534                        is: None,
535                    });
536                }
537            }
538        }
539
540        let mut xml_row = Row {
541            r: row,
542            spans: None,
543            s: None,
544            custom_format: None,
545            ht: None,
546            hidden: None,
547            custom_height: None,
548            outline_level: None,
549            cells,
550        };
551
552        if let Some(opts) = options {
553            if let Some(height) = opts.height {
554                xml_row.ht = Some(height);
555                xml_row.custom_height = Some(true);
556            }
557            if let Some(false) = opts.visible {
558                xml_row.hidden = Some(true);
559            }
560            if let Some(level) = opts.outline_level {
561                if level > 0 {
562                    xml_row.outline_level = Some(level);
563                }
564            }
565            if let Some(sid) = opts.style_id {
566                xml_row.s = Some(sid);
567                xml_row.custom_format = Some(true);
568            }
569        }
570
571        self.rows.push(xml_row);
572        Ok(())
573    }
574}
575
576#[cfg(test)]
577mod tests {
578    use super::*;
579    use sheetkit_xml::worksheet::WorksheetXml;
580
581    #[test]
582    fn test_basic_write_and_finish() {
583        let mut sw = StreamWriter::new("Sheet1");
584        sw.write_row(1, &[CellValue::from("Name"), CellValue::from("Age")])
585            .unwrap();
586        sw.write_row(2, &[CellValue::from("Alice"), CellValue::from(30)])
587            .unwrap();
588        let xml_bytes = sw.finish().unwrap();
589        let xml = String::from_utf8(xml_bytes).unwrap();
590
591        assert!(xml.contains("<?xml version=\"1.0\""));
592        assert!(xml.contains("<worksheet"));
593        assert!(xml.contains("<sheetData>"));
594        assert!(xml.contains("</sheetData>"));
595        assert!(xml.contains("</worksheet>"));
596    }
597
598    #[test]
599    fn test_parse_output_xml_back() {
600        let mut sw = StreamWriter::new("TestSheet");
601        sw.write_row(1, &[CellValue::from("Hello"), CellValue::from(42)])
602            .unwrap();
603        let xml_bytes = sw.finish().unwrap();
604        let xml_str = String::from_utf8(xml_bytes).unwrap();
605
606        // Parse back into WorksheetXml
607        let ws: WorksheetXml = quick_xml::de::from_str(&xml_str).unwrap();
608        assert_eq!(ws.sheet_data.rows.len(), 1);
609        assert_eq!(ws.sheet_data.rows[0].r, 1);
610        assert_eq!(ws.sheet_data.rows[0].cells.len(), 2);
611        // First cell is a shared string (t="s")
612        assert_eq!(
613            ws.sheet_data.rows[0].cells[0].t,
614            sheetkit_xml::worksheet::CellTypeTag::SharedString
615        );
616        assert_eq!(ws.sheet_data.rows[0].cells[0].r, "A1");
617        // Second cell is a number
618        assert_eq!(
619            ws.sheet_data.rows[0].cells[1].t,
620            sheetkit_xml::worksheet::CellTypeTag::None
621        );
622        assert_eq!(ws.sheet_data.rows[0].cells[1].v, Some("42".to_string()));
623        assert_eq!(ws.sheet_data.rows[0].cells[1].r, "B1");
624    }
625
626    #[test]
627    fn test_string_values_populate_sst() {
628        let mut sw = StreamWriter::new("Sheet1");
629        sw.write_row(1, &[CellValue::from("Hello"), CellValue::from("World")])
630            .unwrap();
631
632        assert_eq!(sw.sst().len(), 2);
633        assert_eq!(sw.sst().get(0), Some("Hello"));
634        assert_eq!(sw.sst().get(1), Some("World"));
635    }
636
637    #[test]
638    fn test_number_value() {
639        let mut sw = StreamWriter::new("Sheet1");
640        sw.write_row(1, &[CellValue::from(3.15)]).unwrap();
641        let xml_bytes = sw.finish().unwrap();
642        let xml = String::from_utf8(xml_bytes).unwrap();
643
644        assert!(xml.contains("<v>3.15</v>"));
645    }
646
647    #[test]
648    fn test_bool_values() {
649        let mut sw = StreamWriter::new("Sheet1");
650        sw.write_row(1, &[CellValue::from(true), CellValue::from(false)])
651            .unwrap();
652        let xml_bytes = sw.finish().unwrap();
653        let xml = String::from_utf8(xml_bytes).unwrap();
654
655        assert!(xml.contains("<v>1</v>"));
656        assert!(xml.contains("<v>0</v>"));
657    }
658
659    #[test]
660    fn test_formula_value() {
661        let mut sw = StreamWriter::new("Sheet1");
662        sw.write_row(
663            1,
664            &[CellValue::Formula {
665                expr: "SUM(A2:A10)".to_string(),
666                result: None,
667            }],
668        )
669        .unwrap();
670        let xml_bytes = sw.finish().unwrap();
671        let xml = String::from_utf8(xml_bytes).unwrap();
672
673        assert!(xml.contains("SUM(A2:A10)"));
674    }
675
676    #[test]
677    fn test_error_value() {
678        let mut sw = StreamWriter::new("Sheet1");
679        sw.write_row(1, &[CellValue::Error("#DIV/0!".to_string())])
680            .unwrap();
681        let xml_bytes = sw.finish().unwrap();
682        let xml = String::from_utf8(xml_bytes).unwrap();
683
684        assert!(xml.contains("#DIV/0!"));
685    }
686
687    #[test]
688    fn test_empty_values_are_skipped() {
689        let mut sw = StreamWriter::new("Sheet1");
690        sw.write_row(
691            1,
692            &[CellValue::from("A"), CellValue::Empty, CellValue::from("C")],
693        )
694        .unwrap();
695        let xml_bytes = sw.finish().unwrap();
696        let xml_str = String::from_utf8(xml_bytes).unwrap();
697
698        // Parse and verify only 2 cells present (A1 and C1)
699        let ws: WorksheetXml = quick_xml::de::from_str(&xml_str).unwrap();
700        assert_eq!(ws.sheet_data.rows[0].cells.len(), 2);
701        assert_eq!(ws.sheet_data.rows[0].cells[0].r, "A1");
702        assert_eq!(ws.sheet_data.rows[0].cells[1].r, "C1");
703    }
704
705    #[test]
706    fn test_write_row_with_style() {
707        let mut sw = StreamWriter::new("Sheet1");
708        sw.write_row_with_style(1, &[CellValue::from("Styled")], 5)
709            .unwrap();
710        let xml_bytes = sw.finish().unwrap();
711        let xml = String::from_utf8(xml_bytes).unwrap();
712
713        assert!(xml.contains("s=\"5\""));
714    }
715
716    #[test]
717    fn test_set_col_width_before_rows() {
718        let mut sw = StreamWriter::new("Sheet1");
719        sw.set_col_width(1, 20.0).unwrap();
720        sw.write_row(1, &[CellValue::from("data")]).unwrap();
721        let xml_bytes = sw.finish().unwrap();
722        let xml_str = String::from_utf8(xml_bytes).unwrap();
723
724        let ws: WorksheetXml = quick_xml::de::from_str(&xml_str).unwrap();
725        let cols = ws.cols.unwrap();
726        assert_eq!(cols.cols.len(), 1);
727        assert_eq!(cols.cols[0].min, 1);
728        assert_eq!(cols.cols[0].max, 1);
729        assert_eq!(cols.cols[0].width, Some(20.0));
730        assert_eq!(cols.cols[0].custom_width, Some(true));
731    }
732
733    #[test]
734    fn test_set_col_width_range() {
735        let mut sw = StreamWriter::new("Sheet1");
736        sw.set_col_width_range(1, 3, 15.5).unwrap();
737        let xml_bytes = sw.finish().unwrap();
738        let xml_str = String::from_utf8(xml_bytes).unwrap();
739
740        let ws: WorksheetXml = quick_xml::de::from_str(&xml_str).unwrap();
741        let cols = ws.cols.unwrap();
742        assert_eq!(cols.cols.len(), 1);
743        assert_eq!(cols.cols[0].min, 1);
744        assert_eq!(cols.cols[0].max, 3);
745        assert_eq!(cols.cols[0].width, Some(15.5));
746    }
747
748    #[test]
749    fn test_col_width_in_output_xml() {
750        let mut sw = StreamWriter::new("Sheet1");
751        sw.set_col_width(2, 25.0).unwrap();
752        let xml_bytes = sw.finish().unwrap();
753        let xml_str = String::from_utf8(xml_bytes).unwrap();
754
755        // Verify cols section appears before sheetData
756        let cols_pos = xml_str.find("<cols>").unwrap();
757        let sheet_data_pos = xml_str.find("<sheetData").unwrap();
758        assert!(cols_pos < sheet_data_pos);
759    }
760
761    #[test]
762    fn test_col_width_after_rows_returns_error() {
763        let mut sw = StreamWriter::new("Sheet1");
764        sw.write_row(1, &[CellValue::from("data")]).unwrap();
765        let result = sw.set_col_width(1, 20.0);
766        assert!(result.is_err());
767        assert!(matches!(result.unwrap_err(), Error::StreamColumnsAfterRows));
768    }
769
770    #[test]
771    fn test_rows_in_order_succeeds() {
772        let mut sw = StreamWriter::new("Sheet1");
773        sw.write_row(1, &[CellValue::from("a")]).unwrap();
774        sw.write_row(2, &[CellValue::from("b")]).unwrap();
775        sw.write_row(3, &[CellValue::from("c")]).unwrap();
776        sw.finish().unwrap();
777    }
778
779    #[test]
780    fn test_rows_with_gaps_succeeds() {
781        let mut sw = StreamWriter::new("Sheet1");
782        sw.write_row(1, &[CellValue::from("a")]).unwrap();
783        sw.write_row(3, &[CellValue::from("b")]).unwrap();
784        sw.write_row(5, &[CellValue::from("c")]).unwrap();
785        let xml_bytes = sw.finish().unwrap();
786        let xml_str = String::from_utf8(xml_bytes).unwrap();
787
788        let ws: WorksheetXml = quick_xml::de::from_str(&xml_str).unwrap();
789        assert_eq!(ws.sheet_data.rows.len(), 3);
790        assert_eq!(ws.sheet_data.rows[0].r, 1);
791        assert_eq!(ws.sheet_data.rows[1].r, 3);
792        assert_eq!(ws.sheet_data.rows[2].r, 5);
793    }
794
795    #[test]
796    fn test_duplicate_row_number_fails() {
797        let mut sw = StreamWriter::new("Sheet1");
798        sw.write_row(1, &[CellValue::from("a")]).unwrap();
799        let result = sw.write_row(1, &[CellValue::from("b")]);
800        assert!(result.is_err());
801        assert!(matches!(
802            result.unwrap_err(),
803            Error::StreamRowAlreadyWritten { row: 1 }
804        ));
805    }
806
807    #[test]
808    fn test_row_zero_fails() {
809        let mut sw = StreamWriter::new("Sheet1");
810        let result = sw.write_row(0, &[CellValue::from("a")]);
811        assert!(result.is_err());
812        assert!(matches!(result.unwrap_err(), Error::InvalidRowNumber(0)));
813    }
814
815    #[test]
816    fn test_row_out_of_order_fails() {
817        let mut sw = StreamWriter::new("Sheet1");
818        sw.write_row(5, &[CellValue::from("a")]).unwrap();
819        let result = sw.write_row(3, &[CellValue::from("b")]);
820        assert!(result.is_err());
821        assert!(matches!(
822            result.unwrap_err(),
823            Error::StreamRowAlreadyWritten { row: 3 }
824        ));
825    }
826
827    #[test]
828    fn test_merge_cells_in_output() {
829        let mut sw = StreamWriter::new("Sheet1");
830        sw.write_row(1, &[CellValue::from("Merged")]).unwrap();
831        sw.add_merge_cell("A1:B1").unwrap();
832        let xml_bytes = sw.finish().unwrap();
833        let xml_str = String::from_utf8(xml_bytes).unwrap();
834
835        let ws: WorksheetXml = quick_xml::de::from_str(&xml_str).unwrap();
836        let mc = ws.merge_cells.unwrap();
837        assert_eq!(mc.merge_cells.len(), 1);
838        assert_eq!(mc.merge_cells[0].reference, "A1:B1");
839    }
840
841    #[test]
842    fn test_multiple_merge_cells() {
843        let mut sw = StreamWriter::new("Sheet1");
844        sw.write_row(1, &[CellValue::from("a")]).unwrap();
845        sw.add_merge_cell("A1:B1").unwrap();
846        sw.add_merge_cell("C1:D1").unwrap();
847        let xml_bytes = sw.finish().unwrap();
848        let xml_str = String::from_utf8(xml_bytes).unwrap();
849
850        let ws: WorksheetXml = quick_xml::de::from_str(&xml_str).unwrap();
851        let mc = ws.merge_cells.unwrap();
852        assert_eq!(mc.merge_cells.len(), 2);
853        assert_eq!(mc.merge_cells[0].reference, "A1:B1");
854        assert_eq!(mc.merge_cells[1].reference, "C1:D1");
855    }
856
857    #[test]
858    fn test_finish_twice_returns_error() {
859        let mut sw = StreamWriter::new("Sheet1");
860        sw.write_row(1, &[CellValue::from("a")]).unwrap();
861        sw.finish().unwrap();
862        let result = sw.finish();
863        assert!(result.is_err());
864        assert!(matches!(result.unwrap_err(), Error::StreamAlreadyFinished));
865    }
866
867    #[test]
868    fn test_write_after_finish_returns_error() {
869        let mut sw = StreamWriter::new("Sheet1");
870        sw.write_row(1, &[CellValue::from("a")]).unwrap();
871        sw.finish().unwrap();
872        let result = sw.write_row(2, &[CellValue::from("b")]);
873        assert!(result.is_err());
874        assert!(matches!(result.unwrap_err(), Error::StreamAlreadyFinished));
875    }
876
877    #[test]
878    fn test_finish_with_no_rows() {
879        let mut sw = StreamWriter::new("Sheet1");
880        let xml_bytes = sw.finish().unwrap();
881        let xml_str = String::from_utf8(xml_bytes).unwrap();
882
883        // Should still produce valid XML
884        let ws: WorksheetXml = quick_xml::de::from_str(&xml_str).unwrap();
885        assert!(ws.sheet_data.rows.is_empty());
886    }
887
888    #[test]
889    fn test_finish_with_cols_and_no_rows() {
890        let mut sw = StreamWriter::new("Sheet1");
891        sw.set_col_width(1, 20.0).unwrap();
892        let xml_bytes = sw.finish().unwrap();
893        let xml_str = String::from_utf8(xml_bytes).unwrap();
894
895        let ws: WorksheetXml = quick_xml::de::from_str(&xml_str).unwrap();
896        assert!(ws.cols.is_some());
897        assert!(ws.sheet_data.rows.is_empty());
898    }
899
900    #[test]
901    fn test_into_parts() {
902        let mut sw = StreamWriter::new("Sheet1");
903        sw.write_row(1, &[CellValue::from("Hello")]).unwrap();
904        let (xml_bytes, sst) = sw.into_parts().unwrap();
905
906        assert!(!xml_bytes.is_empty());
907        assert_eq!(sst.len(), 1);
908        assert_eq!(sst.get(0), Some("Hello"));
909    }
910
911    #[test]
912    fn test_sst_deduplication() {
913        let mut sw = StreamWriter::new("Sheet1");
914        sw.write_row(1, &[CellValue::from("same"), CellValue::from("same")])
915            .unwrap();
916        sw.write_row(2, &[CellValue::from("same"), CellValue::from("other")])
917            .unwrap();
918
919        assert_eq!(sw.sst().len(), 2); // "same" and "other"
920        assert_eq!(sw.sst().get(0), Some("same"));
921        assert_eq!(sw.sst().get(1), Some("other"));
922    }
923
924    #[test]
925    fn test_large_scale_10000_rows() {
926        let mut sw = StreamWriter::new("BigSheet");
927        for i in 1..=10_000u32 {
928            sw.write_row(
929                i,
930                &[
931                    CellValue::from(format!("Row {}", i)),
932                    CellValue::from(i as i32),
933                    CellValue::from(i as f64 * 1.5),
934                ],
935            )
936            .unwrap();
937        }
938        let xml_bytes = sw.finish().unwrap();
939        let xml_str = String::from_utf8(xml_bytes).unwrap();
940
941        // Verify it parses
942        let ws: WorksheetXml = quick_xml::de::from_str(&xml_str).unwrap();
943        assert_eq!(ws.sheet_data.rows.len(), 10_000);
944        assert_eq!(ws.sheet_data.rows[0].r, 1);
945        assert_eq!(ws.sheet_data.rows[9999].r, 10_000);
946    }
947
948    #[test]
949    fn test_large_sst_dedup() {
950        let mut sw = StreamWriter::new("Sheet1");
951        // Write 1000 rows, each with the same 3 string values
952        for i in 1..=1000u32 {
953            sw.write_row(
954                i,
955                &[
956                    CellValue::from("Alpha"),
957                    CellValue::from("Beta"),
958                    CellValue::from("Gamma"),
959                ],
960            )
961            .unwrap();
962        }
963        // SST should only have 3 unique strings
964        assert_eq!(sw.sst().len(), 3);
965    }
966
967    #[test]
968    fn test_xml_escape_in_formula() {
969        let mut sw = StreamWriter::new("Sheet1");
970        sw.write_row(
971            1,
972            &[CellValue::Formula {
973                expr: "IF(A1>0,\"yes\",\"no\")".to_string(),
974                result: None,
975            }],
976        )
977        .unwrap();
978        let xml_bytes = sw.finish().unwrap();
979        let xml = String::from_utf8(xml_bytes).unwrap();
980        // quick_xml escapes > in text content; quotes don't require escaping in text content
981        assert!(xml.contains("&gt;"));
982        // Verify the formula round-trips through XML
983        let ws: WorksheetXml = quick_xml::de::from_str(&xml).unwrap();
984        let formula = ws.sheet_data.rows[0].cells[0].f.as_ref().unwrap();
985        assert_eq!(formula.value, Some("IF(A1>0,\"yes\",\"no\")".to_string()));
986    }
987
988    #[test]
989    fn test_add_merge_cell_invalid_reference() {
990        let mut sw = StreamWriter::new("Sheet1");
991        // Missing colon separator.
992        let result = sw.add_merge_cell("A1B2");
993        assert!(result.is_err());
994        assert!(matches!(
995            result.unwrap_err(),
996            Error::InvalidMergeCellReference(_)
997        ));
998    }
999
1000    #[test]
1001    fn test_add_merge_cell_invalid_cell_name() {
1002        let mut sw = StreamWriter::new("Sheet1");
1003        // Invalid cell name before colon.
1004        let result = sw.add_merge_cell("ZZZ:B2");
1005        assert!(result.is_err());
1006        assert!(matches!(
1007            result.unwrap_err(),
1008            Error::InvalidMergeCellReference(_)
1009        ));
1010    }
1011
1012    #[test]
1013    fn test_add_merge_cell_empty_reference() {
1014        let mut sw = StreamWriter::new("Sheet1");
1015        let result = sw.add_merge_cell("");
1016        assert!(result.is_err());
1017        assert!(matches!(
1018            result.unwrap_err(),
1019            Error::InvalidMergeCellReference(_)
1020        ));
1021    }
1022
1023    #[test]
1024    fn test_add_merge_cell_after_finish_fails() {
1025        let mut sw = StreamWriter::new("Sheet1");
1026        sw.finish().unwrap();
1027        let result = sw.add_merge_cell("A1:B1");
1028        assert!(result.is_err());
1029        assert!(matches!(result.unwrap_err(), Error::StreamAlreadyFinished));
1030    }
1031
1032    #[test]
1033    fn test_set_col_width_after_finish_fails() {
1034        let mut sw = StreamWriter::new("Sheet1");
1035        sw.finish().unwrap();
1036        let result = sw.set_col_width(1, 10.0);
1037        assert!(result.is_err());
1038        assert!(matches!(result.unwrap_err(), Error::StreamAlreadyFinished));
1039    }
1040
1041    #[test]
1042    fn test_sheet_name_getter() {
1043        let sw = StreamWriter::new("MySheet");
1044        assert_eq!(sw.sheet_name(), "MySheet");
1045    }
1046
1047    #[test]
1048    fn test_all_value_types_in_single_row() {
1049        let mut sw = StreamWriter::new("Sheet1");
1050        sw.write_row(
1051            1,
1052            &[
1053                CellValue::from("text"),
1054                CellValue::from(42),
1055                CellValue::from(3.15),
1056                CellValue::from(true),
1057                CellValue::from(false),
1058                CellValue::Formula {
1059                    expr: "A1+B1".to_string(),
1060                    result: None,
1061                },
1062                CellValue::Error("#N/A".to_string()),
1063                CellValue::Empty,
1064            ],
1065        )
1066        .unwrap();
1067        let xml_bytes = sw.finish().unwrap();
1068        let xml_str = String::from_utf8(xml_bytes).unwrap();
1069
1070        // Should parse without error
1071        let ws: WorksheetXml = quick_xml::de::from_str(&xml_str).unwrap();
1072        // 7 cells (Empty is skipped)
1073        assert_eq!(ws.sheet_data.rows[0].cells.len(), 7);
1074    }
1075
1076    #[test]
1077    fn test_col_width_invalid_column_zero() {
1078        let mut sw = StreamWriter::new("Sheet1");
1079        let result = sw.set_col_width(0, 10.0);
1080        assert!(result.is_err());
1081        assert!(matches!(result.unwrap_err(), Error::InvalidColumnNumber(0)));
1082    }
1083
1084    #[test]
1085    fn test_write_row_with_options_height() {
1086        let mut sw = StreamWriter::new("Sheet1");
1087        let opts = StreamRowOptions {
1088            height: Some(30.0),
1089            ..Default::default()
1090        };
1091        sw.write_row_with_options(1, &[CellValue::from("tall row")], &opts)
1092            .unwrap();
1093        let xml_bytes = sw.finish().unwrap();
1094        let xml = String::from_utf8(xml_bytes).unwrap();
1095
1096        // Parse back and verify
1097        let ws: WorksheetXml = quick_xml::de::from_str(&xml).unwrap();
1098        assert_eq!(ws.sheet_data.rows[0].ht, Some(30.0));
1099        assert_eq!(ws.sheet_data.rows[0].custom_height, Some(true));
1100    }
1101
1102    #[test]
1103    fn test_write_row_with_options_hidden() {
1104        let mut sw = StreamWriter::new("Sheet1");
1105        let opts = StreamRowOptions {
1106            visible: Some(false),
1107            ..Default::default()
1108        };
1109        sw.write_row_with_options(1, &[CellValue::from("hidden")], &opts)
1110            .unwrap();
1111        let xml_bytes = sw.finish().unwrap();
1112        let xml = String::from_utf8(xml_bytes).unwrap();
1113
1114        let ws: WorksheetXml = quick_xml::de::from_str(&xml).unwrap();
1115        assert_eq!(ws.sheet_data.rows[0].hidden, Some(true));
1116    }
1117
1118    #[test]
1119    fn test_write_row_with_options_outline_level() {
1120        let mut sw = StreamWriter::new("Sheet1");
1121        let opts = StreamRowOptions {
1122            outline_level: Some(3),
1123            ..Default::default()
1124        };
1125        sw.write_row_with_options(1, &[CellValue::from("grouped")], &opts)
1126            .unwrap();
1127        let xml_bytes = sw.finish().unwrap();
1128        let xml = String::from_utf8(xml_bytes).unwrap();
1129
1130        let ws: WorksheetXml = quick_xml::de::from_str(&xml).unwrap();
1131        assert_eq!(ws.sheet_data.rows[0].outline_level, Some(3));
1132    }
1133
1134    #[test]
1135    fn test_write_row_with_options_style() {
1136        let mut sw = StreamWriter::new("Sheet1");
1137        let opts = StreamRowOptions {
1138            style_id: Some(2),
1139            ..Default::default()
1140        };
1141        sw.write_row_with_options(1, &[CellValue::from("styled row")], &opts)
1142            .unwrap();
1143        let xml_bytes = sw.finish().unwrap();
1144        let xml = String::from_utf8(xml_bytes).unwrap();
1145
1146        let ws: WorksheetXml = quick_xml::de::from_str(&xml).unwrap();
1147        assert_eq!(ws.sheet_data.rows[0].s, Some(2));
1148        assert_eq!(ws.sheet_data.rows[0].custom_format, Some(true));
1149    }
1150
1151    #[test]
1152    fn test_write_row_with_options_all() {
1153        let mut sw = StreamWriter::new("Sheet1");
1154        let opts = StreamRowOptions {
1155            height: Some(25.5),
1156            visible: Some(false),
1157            outline_level: Some(2),
1158            style_id: Some(7),
1159        };
1160        sw.write_row_with_options(1, &[CellValue::from("all options")], &opts)
1161            .unwrap();
1162        let xml_bytes = sw.finish().unwrap();
1163        let xml = String::from_utf8(xml_bytes).unwrap();
1164
1165        let ws: WorksheetXml = quick_xml::de::from_str(&xml).unwrap();
1166        let row = &ws.sheet_data.rows[0];
1167        assert_eq!(row.ht, Some(25.5));
1168        assert_eq!(row.custom_height, Some(true));
1169        assert_eq!(row.hidden, Some(true));
1170        assert_eq!(row.outline_level, Some(2));
1171        assert_eq!(row.s, Some(7));
1172        assert_eq!(row.custom_format, Some(true));
1173    }
1174
1175    #[test]
1176    fn test_write_row_with_options_height_exceeded() {
1177        let mut sw = StreamWriter::new("Sheet1");
1178        let opts = StreamRowOptions {
1179            height: Some(500.0),
1180            ..Default::default()
1181        };
1182        let result = sw.write_row_with_options(1, &[CellValue::from("too tall")], &opts);
1183        assert!(result.is_err());
1184        assert!(matches!(
1185            result.unwrap_err(),
1186            Error::RowHeightExceeded { .. }
1187        ));
1188    }
1189
1190    #[test]
1191    fn test_write_row_with_options_outline_level_exceeded() {
1192        let mut sw = StreamWriter::new("Sheet1");
1193        let opts = StreamRowOptions {
1194            outline_level: Some(8),
1195            ..Default::default()
1196        };
1197        let result = sw.write_row_with_options(1, &[CellValue::from("bad level")], &opts);
1198        assert!(result.is_err());
1199    }
1200
1201    #[test]
1202    fn test_write_row_with_options_visible_true_no_hidden_attr() {
1203        let mut sw = StreamWriter::new("Sheet1");
1204        let opts = StreamRowOptions {
1205            visible: Some(true),
1206            ..Default::default()
1207        };
1208        sw.write_row_with_options(1, &[CellValue::from("visible")], &opts)
1209            .unwrap();
1210        let xml_bytes = sw.finish().unwrap();
1211        let xml = String::from_utf8(xml_bytes).unwrap();
1212
1213        // visible=true should NOT produce hidden attribute
1214        assert!(!xml.contains("hidden="));
1215    }
1216
1217    #[test]
1218    fn test_col_style() {
1219        let mut sw = StreamWriter::new("Sheet1");
1220        sw.set_col_style(1, 3).unwrap();
1221        sw.write_row(1, &[CellValue::from("data")]).unwrap();
1222        let xml_bytes = sw.finish().unwrap();
1223        let xml = String::from_utf8(xml_bytes).unwrap();
1224
1225        let ws: WorksheetXml = quick_xml::de::from_str(&xml).unwrap();
1226        let cols = ws.cols.unwrap();
1227        let styled_col = cols.cols.iter().find(|c| c.style.is_some()).unwrap();
1228        assert_eq!(styled_col.style, Some(3));
1229        assert_eq!(styled_col.min, 1);
1230        assert_eq!(styled_col.max, 1);
1231    }
1232
1233    #[test]
1234    fn test_col_visible() {
1235        let mut sw = StreamWriter::new("Sheet1");
1236        sw.set_col_visible(2, false).unwrap();
1237        sw.write_row(1, &[CellValue::from("a"), CellValue::from("b")])
1238            .unwrap();
1239        let xml_bytes = sw.finish().unwrap();
1240        let xml = String::from_utf8(xml_bytes).unwrap();
1241
1242        let ws: WorksheetXml = quick_xml::de::from_str(&xml).unwrap();
1243        let cols = ws.cols.unwrap();
1244        let hidden_col = cols.cols.iter().find(|c| c.hidden == Some(true)).unwrap();
1245        assert_eq!(hidden_col.min, 2);
1246        assert_eq!(hidden_col.max, 2);
1247    }
1248
1249    #[test]
1250    fn test_col_outline_level() {
1251        let mut sw = StreamWriter::new("Sheet1");
1252        sw.set_col_outline_level(3, 2).unwrap();
1253        sw.write_row(1, &[CellValue::from("a")]).unwrap();
1254        let xml_bytes = sw.finish().unwrap();
1255        let xml = String::from_utf8(xml_bytes).unwrap();
1256
1257        let ws: WorksheetXml = quick_xml::de::from_str(&xml).unwrap();
1258        let cols = ws.cols.unwrap();
1259        let outlined_col = cols
1260            .cols
1261            .iter()
1262            .find(|c| c.outline_level.is_some())
1263            .unwrap();
1264        assert_eq!(outlined_col.outline_level, Some(2));
1265        assert_eq!(outlined_col.min, 3);
1266        assert_eq!(outlined_col.max, 3);
1267    }
1268
1269    #[test]
1270    fn test_col_outline_level_exceeded() {
1271        let mut sw = StreamWriter::new("Sheet1");
1272        let result = sw.set_col_outline_level(1, 8);
1273        assert!(result.is_err());
1274    }
1275
1276    #[test]
1277    fn test_col_style_after_rows_error() {
1278        let mut sw = StreamWriter::new("Sheet1");
1279        sw.write_row(1, &[CellValue::from("data")]).unwrap();
1280        let result = sw.set_col_style(1, 1);
1281        assert!(result.is_err());
1282        assert!(matches!(result.unwrap_err(), Error::StreamColumnsAfterRows));
1283    }
1284
1285    #[test]
1286    fn test_col_visible_after_rows_error() {
1287        let mut sw = StreamWriter::new("Sheet1");
1288        sw.write_row(1, &[CellValue::from("data")]).unwrap();
1289        let result = sw.set_col_visible(1, false);
1290        assert!(result.is_err());
1291        assert!(matches!(result.unwrap_err(), Error::StreamColumnsAfterRows));
1292    }
1293
1294    #[test]
1295    fn test_col_outline_after_rows_error() {
1296        let mut sw = StreamWriter::new("Sheet1");
1297        sw.write_row(1, &[CellValue::from("data")]).unwrap();
1298        let result = sw.set_col_outline_level(1, 1);
1299        assert!(result.is_err());
1300        assert!(matches!(result.unwrap_err(), Error::StreamColumnsAfterRows));
1301    }
1302
1303    #[test]
1304    fn test_freeze_panes_rows() {
1305        let mut sw = StreamWriter::new("Sheet1");
1306        sw.set_freeze_panes("A2").unwrap();
1307        sw.write_row(1, &[CellValue::from("header")]).unwrap();
1308        sw.write_row(2, &[CellValue::from("data")]).unwrap();
1309        let xml_bytes = sw.finish().unwrap();
1310        let xml_str = String::from_utf8(xml_bytes).unwrap();
1311
1312        let ws: WorksheetXml = quick_xml::de::from_str(&xml_str).unwrap();
1313        let views = ws.sheet_views.unwrap();
1314        let pane = views.sheet_views[0].pane.as_ref().unwrap();
1315        assert_eq!(pane.y_split, Some(1));
1316        assert_eq!(pane.top_left_cell, Some("A2".to_string()));
1317        assert_eq!(pane.active_pane, Some("bottomLeft".to_string()));
1318        assert_eq!(pane.state, Some("frozen".to_string()));
1319        // xSplit should not appear when only rows are frozen
1320        assert_eq!(pane.x_split, None);
1321    }
1322
1323    #[test]
1324    fn test_freeze_panes_cols() {
1325        let mut sw = StreamWriter::new("Sheet1");
1326        sw.set_freeze_panes("B1").unwrap();
1327        sw.write_row(1, &[CellValue::from("a"), CellValue::from("b")])
1328            .unwrap();
1329        let xml_bytes = sw.finish().unwrap();
1330        let xml_str = String::from_utf8(xml_bytes).unwrap();
1331
1332        let ws: WorksheetXml = quick_xml::de::from_str(&xml_str).unwrap();
1333        let views = ws.sheet_views.unwrap();
1334        let pane = views.sheet_views[0].pane.as_ref().unwrap();
1335        assert_eq!(pane.x_split, Some(1));
1336        assert_eq!(pane.top_left_cell, Some("B1".to_string()));
1337        assert_eq!(pane.active_pane, Some("topRight".to_string()));
1338        assert_eq!(pane.state, Some("frozen".to_string()));
1339        // ySplit should not appear when only cols are frozen
1340        assert_eq!(pane.y_split, None);
1341    }
1342
1343    #[test]
1344    fn test_freeze_panes_both() {
1345        let mut sw = StreamWriter::new("Sheet1");
1346        sw.set_freeze_panes("C3").unwrap();
1347        sw.write_row(1, &[CellValue::from("a")]).unwrap();
1348        let xml_bytes = sw.finish().unwrap();
1349        let xml_str = String::from_utf8(xml_bytes).unwrap();
1350
1351        let ws: WorksheetXml = quick_xml::de::from_str(&xml_str).unwrap();
1352        let views = ws.sheet_views.unwrap();
1353        let pane = views.sheet_views[0].pane.as_ref().unwrap();
1354        assert_eq!(pane.x_split, Some(2));
1355        assert_eq!(pane.y_split, Some(2));
1356        assert_eq!(pane.top_left_cell, Some("C3".to_string()));
1357        assert_eq!(pane.active_pane, Some("bottomRight".to_string()));
1358        assert_eq!(pane.state, Some("frozen".to_string()));
1359    }
1360
1361    #[test]
1362    fn test_freeze_panes_after_rows_error() {
1363        let mut sw = StreamWriter::new("Sheet1");
1364        sw.write_row(1, &[CellValue::from("data")]).unwrap();
1365        let result = sw.set_freeze_panes("A2");
1366        assert!(result.is_err());
1367        assert!(matches!(result.unwrap_err(), Error::StreamColumnsAfterRows));
1368    }
1369
1370    #[test]
1371    fn test_freeze_panes_a1_error() {
1372        let mut sw = StreamWriter::new("Sheet1");
1373        let result = sw.set_freeze_panes("A1");
1374        assert!(result.is_err());
1375        assert!(matches!(
1376            result.unwrap_err(),
1377            Error::InvalidCellReference(_)
1378        ));
1379    }
1380
1381    #[test]
1382    fn test_freeze_panes_invalid_cell_error() {
1383        let mut sw = StreamWriter::new("Sheet1");
1384        let result = sw.set_freeze_panes("ZZZZ1");
1385        assert!(result.is_err());
1386    }
1387
1388    #[test]
1389    fn test_freeze_panes_appears_before_cols() {
1390        let mut sw = StreamWriter::new("Sheet1");
1391        sw.set_freeze_panes("A2").unwrap();
1392        sw.set_col_width(1, 20.0).unwrap();
1393        sw.write_row(1, &[CellValue::from("data")]).unwrap();
1394        let xml_bytes = sw.finish().unwrap();
1395        let xml = String::from_utf8(xml_bytes).unwrap();
1396
1397        let views_pos = xml.find("<sheetView").unwrap();
1398        let cols_pos = xml.find("<cols>").unwrap();
1399        let data_pos = xml.find("<sheetData").unwrap();
1400        assert!(views_pos < cols_pos);
1401        assert!(cols_pos < data_pos);
1402    }
1403
1404    #[test]
1405    fn test_freeze_panes_after_finish_error() {
1406        let mut sw = StreamWriter::new("Sheet1");
1407        sw.finish().unwrap();
1408        let result = sw.set_freeze_panes("A2");
1409        assert!(result.is_err());
1410        assert!(matches!(result.unwrap_err(), Error::StreamAlreadyFinished));
1411    }
1412
1413    #[test]
1414    fn test_no_freeze_panes_no_sheet_views() {
1415        let mut sw = StreamWriter::new("Sheet1");
1416        sw.write_row(1, &[CellValue::from("data")]).unwrap();
1417        let xml_bytes = sw.finish().unwrap();
1418        let xml = String::from_utf8(xml_bytes).unwrap();
1419
1420        // When no freeze panes are set, sheetViews should not appear
1421        assert!(!xml.contains("<sheetView"));
1422    }
1423
1424    #[test]
1425    fn test_write_row_backward_compat() {
1426        // Ensure the original write_row still works exactly as before.
1427        let mut sw = StreamWriter::new("Sheet1");
1428        sw.write_row(1, &[CellValue::from("hello")]).unwrap();
1429        let xml_bytes = sw.finish().unwrap();
1430        let xml = String::from_utf8(xml_bytes).unwrap();
1431
1432        let ws: WorksheetXml = quick_xml::de::from_str(&xml).unwrap();
1433        let row = &ws.sheet_data.rows[0];
1434        assert_eq!(row.r, 1);
1435        // Should not have any row-level attributes beyond r
1436        assert!(row.ht.is_none());
1437        assert!(row.hidden.is_none());
1438        assert!(row.outline_level.is_none());
1439        assert!(row.custom_format.is_none());
1440    }
1441
1442    #[test]
1443    fn test_write_row_with_style_backward_compat() {
1444        // Ensure the original write_row_with_style still works.
1445        let mut sw = StreamWriter::new("Sheet1");
1446        sw.write_row_with_style(1, &[CellValue::from("styled")], 5)
1447            .unwrap();
1448        let xml_bytes = sw.finish().unwrap();
1449        let xml = String::from_utf8(xml_bytes).unwrap();
1450
1451        // Cell should have s="5" attribute
1452        assert!(xml.contains("s=\"5\""));
1453        // Row should not have row-level style attributes
1454        let ws: WorksheetXml = quick_xml::de::from_str(&xml).unwrap();
1455        assert!(ws.sheet_data.rows[0].custom_format.is_none());
1456    }
1457
1458    #[test]
1459    fn test_col_options_combined_with_widths() {
1460        let mut sw = StreamWriter::new("Sheet1");
1461        sw.set_col_width(1, 20.0).unwrap();
1462        sw.set_col_style(2, 5).unwrap();
1463        sw.set_col_visible(3, false).unwrap();
1464        sw.write_row(
1465            1,
1466            &[
1467                CellValue::from("a"),
1468                CellValue::from("b"),
1469                CellValue::from("c"),
1470            ],
1471        )
1472        .unwrap();
1473        let xml_bytes = sw.finish().unwrap();
1474        let xml = String::from_utf8(xml_bytes).unwrap();
1475
1476        let ws: WorksheetXml = quick_xml::de::from_str(&xml).unwrap();
1477        let cols = ws.cols.unwrap();
1478        assert_eq!(cols.cols.len(), 3);
1479    }
1480
1481    #[test]
1482    fn test_into_worksheet_parts_basic() {
1483        let mut sw = StreamWriter::new("Sheet1");
1484        sw.write_row(1, &[CellValue::from("Hello"), CellValue::from(42)])
1485            .unwrap();
1486        sw.write_row(2, &[CellValue::from("World"), CellValue::from(99)])
1487            .unwrap();
1488        let (ws, sst) = sw.into_worksheet_parts().unwrap();
1489        assert_eq!(ws.sheet_data.rows.len(), 2);
1490        assert_eq!(ws.sheet_data.rows[0].r, 1);
1491        assert_eq!(ws.sheet_data.rows[0].cells.len(), 2);
1492        assert_eq!(ws.sheet_data.rows[1].r, 2);
1493        assert_eq!(sst.len(), 2);
1494    }
1495
1496    #[test]
1497    fn test_into_worksheet_parts_sst() {
1498        let mut sw = StreamWriter::new("Sheet1");
1499        sw.write_row(1, &[CellValue::from("Alpha"), CellValue::from("Beta")])
1500            .unwrap();
1501        let (ws, sst) = sw.into_worksheet_parts().unwrap();
1502        assert_eq!(sst.len(), 2);
1503        assert_eq!(sst.get(0), Some("Alpha"));
1504        assert_eq!(sst.get(1), Some("Beta"));
1505        // Cells should reference SST indices
1506        assert_eq!(ws.sheet_data.rows[0].cells[0].v, Some("0".to_string()));
1507        assert_eq!(ws.sheet_data.rows[0].cells[1].v, Some("1".to_string()));
1508        assert_eq!(
1509            ws.sheet_data.rows[0].cells[0].t,
1510            sheetkit_xml::worksheet::CellTypeTag::SharedString
1511        );
1512    }
1513
1514    #[test]
1515    fn test_into_worksheet_parts_cell_col_set() {
1516        let mut sw = StreamWriter::new("Sheet1");
1517        sw.write_row(
1518            1,
1519            &[CellValue::from("A"), CellValue::Empty, CellValue::from("C")],
1520        )
1521        .unwrap();
1522        let (ws, _) = sw.into_worksheet_parts().unwrap();
1523        assert_eq!(ws.sheet_data.rows[0].cells.len(), 2);
1524        assert_eq!(ws.sheet_data.rows[0].cells[0].col, 1);
1525        assert_eq!(ws.sheet_data.rows[0].cells[0].r.as_str(), "A1");
1526        assert_eq!(ws.sheet_data.rows[0].cells[1].col, 3);
1527        assert_eq!(ws.sheet_data.rows[0].cells[1].r.as_str(), "C1");
1528    }
1529
1530    #[test]
1531    fn test_into_worksheet_parts_freeze_panes() {
1532        let mut sw = StreamWriter::new("Sheet1");
1533        sw.set_freeze_panes("C3").unwrap();
1534        sw.write_row(1, &[CellValue::from("data")]).unwrap();
1535        let (ws, _) = sw.into_worksheet_parts().unwrap();
1536        assert!(ws.sheet_views.is_some());
1537        let views = ws.sheet_views.unwrap();
1538        let pane = views.sheet_views[0].pane.as_ref().unwrap();
1539        assert_eq!(pane.x_split, Some(2));
1540        assert_eq!(pane.y_split, Some(2));
1541        assert_eq!(pane.top_left_cell, Some("C3".to_string()));
1542        assert_eq!(pane.state, Some("frozen".to_string()));
1543    }
1544
1545    #[test]
1546    fn test_into_worksheet_parts_cols() {
1547        let mut sw = StreamWriter::new("Sheet1");
1548        sw.set_col_width(1, 20.0).unwrap();
1549        sw.set_col_style(2, 5).unwrap();
1550        sw.write_row(1, &[CellValue::from("data")]).unwrap();
1551        let (ws, _) = sw.into_worksheet_parts().unwrap();
1552        assert!(ws.cols.is_some());
1553        let cols = ws.cols.unwrap();
1554        assert_eq!(cols.cols.len(), 2);
1555    }
1556
1557    #[test]
1558    fn test_into_worksheet_parts_merge_cells() {
1559        let mut sw = StreamWriter::new("Sheet1");
1560        sw.write_row(1, &[CellValue::from("Merged")]).unwrap();
1561        sw.add_merge_cell("A1:B1").unwrap();
1562        let (ws, _) = sw.into_worksheet_parts().unwrap();
1563        assert!(ws.merge_cells.is_some());
1564        let mc = ws.merge_cells.unwrap();
1565        assert_eq!(mc.merge_cells.len(), 1);
1566        assert_eq!(mc.merge_cells[0].reference, "A1:B1");
1567    }
1568}