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                        zoom_scale: None,
315                        workbook_view_id: 0,
316                        pane: Some(Pane {
317                            x_split: if *x_split > 0 { Some(*x_split) } else { None },
318                            y_split: if *y_split > 0 { Some(*y_split) } else { None },
319                            top_left_cell: Some(top_left_cell.clone()),
320                            active_pane: Some(active_pane.to_string()),
321                            state: Some("frozen".to_string()),
322                        }),
323                        selection: vec![],
324                    }],
325                }
326            });
327
328        let cols = {
329            let has_widths = !self.col_widths.is_empty();
330            let has_options = !self.col_options.is_empty();
331            if has_widths || has_options {
332                let mut col_defs = Vec::new();
333                for &(min, max, width) in &self.col_widths {
334                    col_defs.push(Col {
335                        min,
336                        max,
337                        width: Some(width),
338                        style: None,
339                        hidden: None,
340                        custom_width: Some(true),
341                        outline_level: None,
342                    });
343                }
344                for &(col_num, ref opts) in &self.col_options {
345                    col_defs.push(Col {
346                        min: col_num,
347                        max: col_num,
348                        width: opts.width,
349                        style: opts.style_id,
350                        hidden: opts.hidden,
351                        custom_width: if opts.width.is_some() {
352                            Some(true)
353                        } else {
354                            None
355                        },
356                        outline_level: opts.outline_level.filter(|&l| l > 0),
357                    });
358                }
359                Some(Cols { cols: col_defs })
360            } else {
361                None
362            }
363        };
364
365        let merge_cells = if self.merge_cells.is_empty() {
366            None
367        } else {
368            Some(MergeCells {
369                count: Some(self.merge_cells.len() as u32),
370                merge_cells: self
371                    .merge_cells
372                    .iter()
373                    .map(|r| MergeCell {
374                        reference: r.clone(),
375                    })
376                    .collect(),
377            })
378        };
379
380        WorksheetXml {
381            sheet_views,
382            cols,
383            sheet_data: SheetData { rows },
384            merge_cells,
385            ..WorksheetXml::default()
386        }
387    }
388
389    /// Internal unified implementation for writing a row.
390    fn write_row_impl(
391        &mut self,
392        row: u32,
393        values: &[CellValue],
394        cell_style_id: Option<u32>,
395        options: Option<&StreamRowOptions>,
396    ) -> Result<()> {
397        if self.finished {
398            return Err(Error::StreamAlreadyFinished);
399        }
400
401        // Validate row number.
402        if row == 0 || row > MAX_ROWS {
403            return Err(Error::InvalidRowNumber(row));
404        }
405
406        // Enforce ascending row order.
407        if row <= self.last_row {
408            return Err(Error::StreamRowAlreadyWritten { row });
409        }
410
411        // Validate column count.
412        if values.len() > MAX_COLUMNS as usize {
413            return Err(Error::InvalidColumnNumber(values.len() as u32));
414        }
415
416        // Validate row options if provided.
417        if let Some(opts) = options {
418            if let Some(height) = opts.height {
419                if height > MAX_ROW_HEIGHT {
420                    return Err(Error::RowHeightExceeded {
421                        height,
422                        max: MAX_ROW_HEIGHT,
423                    });
424                }
425            }
426            if let Some(level) = opts.outline_level {
427                if level > MAX_OUTLINE_LEVEL {
428                    return Err(Error::OutlineLevelExceeded {
429                        level,
430                        max: MAX_OUTLINE_LEVEL,
431                    });
432                }
433            }
434        }
435
436        self.started = true;
437        self.last_row = row;
438
439        let mut cells = Vec::new();
440
441        for (i, value) in values.iter().enumerate() {
442            let col = (i as u32) + 1;
443
444            match value {
445                CellValue::Empty => continue,
446                CellValue::String(s) => {
447                    let idx = self.sst.add(s);
448                    cells.push(Cell {
449                        r: CompactCellRef::from_coordinates(col, row),
450                        col,
451                        s: cell_style_id,
452                        t: CellTypeTag::SharedString,
453                        v: Some(idx.to_string()),
454                        f: None,
455                        is: None,
456                    });
457                }
458                CellValue::Number(n) => {
459                    cells.push(Cell {
460                        r: CompactCellRef::from_coordinates(col, row),
461                        col,
462                        s: cell_style_id,
463                        t: CellTypeTag::None,
464                        v: Some(n.to_string()),
465                        f: None,
466                        is: None,
467                    });
468                }
469                CellValue::Date(serial) => {
470                    cells.push(Cell {
471                        r: CompactCellRef::from_coordinates(col, row),
472                        col,
473                        s: cell_style_id,
474                        t: CellTypeTag::None,
475                        v: Some(serial.to_string()),
476                        f: None,
477                        is: None,
478                    });
479                }
480                CellValue::Bool(b) => {
481                    let val = if *b { "1" } else { "0" };
482                    cells.push(Cell {
483                        r: CompactCellRef::from_coordinates(col, row),
484                        col,
485                        s: cell_style_id,
486                        t: CellTypeTag::Boolean,
487                        v: Some(val.to_string()),
488                        f: None,
489                        is: None,
490                    });
491                }
492                CellValue::Formula { expr, .. } => {
493                    cells.push(Cell {
494                        r: CompactCellRef::from_coordinates(col, row),
495                        col,
496                        s: cell_style_id,
497                        t: CellTypeTag::None,
498                        v: None,
499                        f: Some(CellFormula {
500                            t: None,
501                            reference: None,
502                            si: None,
503                            value: Some(expr.clone()),
504                        }),
505                        is: None,
506                    });
507                }
508                CellValue::Error(e) => {
509                    cells.push(Cell {
510                        r: CompactCellRef::from_coordinates(col, row),
511                        col,
512                        s: cell_style_id,
513                        t: CellTypeTag::Error,
514                        v: Some(e.clone()),
515                        f: None,
516                        is: None,
517                    });
518                }
519                CellValue::RichString(runs) => {
520                    let plain = crate::rich_text::rich_text_to_plain(runs);
521                    let idx = self.sst.add(&plain);
522                    cells.push(Cell {
523                        r: CompactCellRef::from_coordinates(col, row),
524                        col,
525                        s: cell_style_id,
526                        t: CellTypeTag::SharedString,
527                        v: Some(idx.to_string()),
528                        f: None,
529                        is: None,
530                    });
531                }
532            }
533        }
534
535        let mut xml_row = Row {
536            r: row,
537            spans: None,
538            s: None,
539            custom_format: None,
540            ht: None,
541            hidden: None,
542            custom_height: None,
543            outline_level: None,
544            cells,
545        };
546
547        if let Some(opts) = options {
548            if let Some(height) = opts.height {
549                xml_row.ht = Some(height);
550                xml_row.custom_height = Some(true);
551            }
552            if let Some(false) = opts.visible {
553                xml_row.hidden = Some(true);
554            }
555            if let Some(level) = opts.outline_level {
556                if level > 0 {
557                    xml_row.outline_level = Some(level);
558                }
559            }
560            if let Some(sid) = opts.style_id {
561                xml_row.s = Some(sid);
562                xml_row.custom_format = Some(true);
563            }
564        }
565
566        self.rows.push(xml_row);
567        Ok(())
568    }
569}
570
571#[cfg(test)]
572mod tests {
573    use super::*;
574    use sheetkit_xml::worksheet::WorksheetXml;
575
576    #[test]
577    fn test_basic_write_and_finish() {
578        let mut sw = StreamWriter::new("Sheet1");
579        sw.write_row(1, &[CellValue::from("Name"), CellValue::from("Age")])
580            .unwrap();
581        sw.write_row(2, &[CellValue::from("Alice"), CellValue::from(30)])
582            .unwrap();
583        let xml_bytes = sw.finish().unwrap();
584        let xml = String::from_utf8(xml_bytes).unwrap();
585
586        assert!(xml.contains("<?xml version=\"1.0\""));
587        assert!(xml.contains("<worksheet"));
588        assert!(xml.contains("<sheetData>"));
589        assert!(xml.contains("</sheetData>"));
590        assert!(xml.contains("</worksheet>"));
591    }
592
593    #[test]
594    fn test_parse_output_xml_back() {
595        let mut sw = StreamWriter::new("TestSheet");
596        sw.write_row(1, &[CellValue::from("Hello"), CellValue::from(42)])
597            .unwrap();
598        let xml_bytes = sw.finish().unwrap();
599        let xml_str = String::from_utf8(xml_bytes).unwrap();
600
601        // Parse back into WorksheetXml
602        let ws: WorksheetXml = quick_xml::de::from_str(&xml_str).unwrap();
603        assert_eq!(ws.sheet_data.rows.len(), 1);
604        assert_eq!(ws.sheet_data.rows[0].r, 1);
605        assert_eq!(ws.sheet_data.rows[0].cells.len(), 2);
606        // First cell is a shared string (t="s")
607        assert_eq!(
608            ws.sheet_data.rows[0].cells[0].t,
609            sheetkit_xml::worksheet::CellTypeTag::SharedString
610        );
611        assert_eq!(ws.sheet_data.rows[0].cells[0].r, "A1");
612        // Second cell is a number
613        assert_eq!(
614            ws.sheet_data.rows[0].cells[1].t,
615            sheetkit_xml::worksheet::CellTypeTag::None
616        );
617        assert_eq!(ws.sheet_data.rows[0].cells[1].v, Some("42".to_string()));
618        assert_eq!(ws.sheet_data.rows[0].cells[1].r, "B1");
619    }
620
621    #[test]
622    fn test_string_values_populate_sst() {
623        let mut sw = StreamWriter::new("Sheet1");
624        sw.write_row(1, &[CellValue::from("Hello"), CellValue::from("World")])
625            .unwrap();
626
627        assert_eq!(sw.sst().len(), 2);
628        assert_eq!(sw.sst().get(0), Some("Hello"));
629        assert_eq!(sw.sst().get(1), Some("World"));
630    }
631
632    #[test]
633    fn test_number_value() {
634        let mut sw = StreamWriter::new("Sheet1");
635        sw.write_row(1, &[CellValue::from(3.15)]).unwrap();
636        let xml_bytes = sw.finish().unwrap();
637        let xml = String::from_utf8(xml_bytes).unwrap();
638
639        assert!(xml.contains("<v>3.15</v>"));
640    }
641
642    #[test]
643    fn test_bool_values() {
644        let mut sw = StreamWriter::new("Sheet1");
645        sw.write_row(1, &[CellValue::from(true), CellValue::from(false)])
646            .unwrap();
647        let xml_bytes = sw.finish().unwrap();
648        let xml = String::from_utf8(xml_bytes).unwrap();
649
650        assert!(xml.contains("<v>1</v>"));
651        assert!(xml.contains("<v>0</v>"));
652    }
653
654    #[test]
655    fn test_formula_value() {
656        let mut sw = StreamWriter::new("Sheet1");
657        sw.write_row(
658            1,
659            &[CellValue::Formula {
660                expr: "SUM(A2:A10)".to_string(),
661                result: None,
662            }],
663        )
664        .unwrap();
665        let xml_bytes = sw.finish().unwrap();
666        let xml = String::from_utf8(xml_bytes).unwrap();
667
668        assert!(xml.contains("SUM(A2:A10)"));
669    }
670
671    #[test]
672    fn test_error_value() {
673        let mut sw = StreamWriter::new("Sheet1");
674        sw.write_row(1, &[CellValue::Error("#DIV/0!".to_string())])
675            .unwrap();
676        let xml_bytes = sw.finish().unwrap();
677        let xml = String::from_utf8(xml_bytes).unwrap();
678
679        assert!(xml.contains("#DIV/0!"));
680    }
681
682    #[test]
683    fn test_empty_values_are_skipped() {
684        let mut sw = StreamWriter::new("Sheet1");
685        sw.write_row(
686            1,
687            &[CellValue::from("A"), CellValue::Empty, CellValue::from("C")],
688        )
689        .unwrap();
690        let xml_bytes = sw.finish().unwrap();
691        let xml_str = String::from_utf8(xml_bytes).unwrap();
692
693        // Parse and verify only 2 cells present (A1 and C1)
694        let ws: WorksheetXml = quick_xml::de::from_str(&xml_str).unwrap();
695        assert_eq!(ws.sheet_data.rows[0].cells.len(), 2);
696        assert_eq!(ws.sheet_data.rows[0].cells[0].r, "A1");
697        assert_eq!(ws.sheet_data.rows[0].cells[1].r, "C1");
698    }
699
700    #[test]
701    fn test_write_row_with_style() {
702        let mut sw = StreamWriter::new("Sheet1");
703        sw.write_row_with_style(1, &[CellValue::from("Styled")], 5)
704            .unwrap();
705        let xml_bytes = sw.finish().unwrap();
706        let xml = String::from_utf8(xml_bytes).unwrap();
707
708        assert!(xml.contains("s=\"5\""));
709    }
710
711    #[test]
712    fn test_set_col_width_before_rows() {
713        let mut sw = StreamWriter::new("Sheet1");
714        sw.set_col_width(1, 20.0).unwrap();
715        sw.write_row(1, &[CellValue::from("data")]).unwrap();
716        let xml_bytes = sw.finish().unwrap();
717        let xml_str = String::from_utf8(xml_bytes).unwrap();
718
719        let ws: WorksheetXml = quick_xml::de::from_str(&xml_str).unwrap();
720        let cols = ws.cols.unwrap();
721        assert_eq!(cols.cols.len(), 1);
722        assert_eq!(cols.cols[0].min, 1);
723        assert_eq!(cols.cols[0].max, 1);
724        assert_eq!(cols.cols[0].width, Some(20.0));
725        assert_eq!(cols.cols[0].custom_width, Some(true));
726    }
727
728    #[test]
729    fn test_set_col_width_range() {
730        let mut sw = StreamWriter::new("Sheet1");
731        sw.set_col_width_range(1, 3, 15.5).unwrap();
732        let xml_bytes = sw.finish().unwrap();
733        let xml_str = String::from_utf8(xml_bytes).unwrap();
734
735        let ws: WorksheetXml = quick_xml::de::from_str(&xml_str).unwrap();
736        let cols = ws.cols.unwrap();
737        assert_eq!(cols.cols.len(), 1);
738        assert_eq!(cols.cols[0].min, 1);
739        assert_eq!(cols.cols[0].max, 3);
740        assert_eq!(cols.cols[0].width, Some(15.5));
741    }
742
743    #[test]
744    fn test_col_width_in_output_xml() {
745        let mut sw = StreamWriter::new("Sheet1");
746        sw.set_col_width(2, 25.0).unwrap();
747        let xml_bytes = sw.finish().unwrap();
748        let xml_str = String::from_utf8(xml_bytes).unwrap();
749
750        // Verify cols section appears before sheetData
751        let cols_pos = xml_str.find("<cols>").unwrap();
752        let sheet_data_pos = xml_str.find("<sheetData").unwrap();
753        assert!(cols_pos < sheet_data_pos);
754    }
755
756    #[test]
757    fn test_col_width_after_rows_returns_error() {
758        let mut sw = StreamWriter::new("Sheet1");
759        sw.write_row(1, &[CellValue::from("data")]).unwrap();
760        let result = sw.set_col_width(1, 20.0);
761        assert!(result.is_err());
762        assert!(matches!(result.unwrap_err(), Error::StreamColumnsAfterRows));
763    }
764
765    #[test]
766    fn test_rows_in_order_succeeds() {
767        let mut sw = StreamWriter::new("Sheet1");
768        sw.write_row(1, &[CellValue::from("a")]).unwrap();
769        sw.write_row(2, &[CellValue::from("b")]).unwrap();
770        sw.write_row(3, &[CellValue::from("c")]).unwrap();
771        sw.finish().unwrap();
772    }
773
774    #[test]
775    fn test_rows_with_gaps_succeeds() {
776        let mut sw = StreamWriter::new("Sheet1");
777        sw.write_row(1, &[CellValue::from("a")]).unwrap();
778        sw.write_row(3, &[CellValue::from("b")]).unwrap();
779        sw.write_row(5, &[CellValue::from("c")]).unwrap();
780        let xml_bytes = sw.finish().unwrap();
781        let xml_str = String::from_utf8(xml_bytes).unwrap();
782
783        let ws: WorksheetXml = quick_xml::de::from_str(&xml_str).unwrap();
784        assert_eq!(ws.sheet_data.rows.len(), 3);
785        assert_eq!(ws.sheet_data.rows[0].r, 1);
786        assert_eq!(ws.sheet_data.rows[1].r, 3);
787        assert_eq!(ws.sheet_data.rows[2].r, 5);
788    }
789
790    #[test]
791    fn test_duplicate_row_number_fails() {
792        let mut sw = StreamWriter::new("Sheet1");
793        sw.write_row(1, &[CellValue::from("a")]).unwrap();
794        let result = sw.write_row(1, &[CellValue::from("b")]);
795        assert!(result.is_err());
796        assert!(matches!(
797            result.unwrap_err(),
798            Error::StreamRowAlreadyWritten { row: 1 }
799        ));
800    }
801
802    #[test]
803    fn test_row_zero_fails() {
804        let mut sw = StreamWriter::new("Sheet1");
805        let result = sw.write_row(0, &[CellValue::from("a")]);
806        assert!(result.is_err());
807        assert!(matches!(result.unwrap_err(), Error::InvalidRowNumber(0)));
808    }
809
810    #[test]
811    fn test_row_out_of_order_fails() {
812        let mut sw = StreamWriter::new("Sheet1");
813        sw.write_row(5, &[CellValue::from("a")]).unwrap();
814        let result = sw.write_row(3, &[CellValue::from("b")]);
815        assert!(result.is_err());
816        assert!(matches!(
817            result.unwrap_err(),
818            Error::StreamRowAlreadyWritten { row: 3 }
819        ));
820    }
821
822    #[test]
823    fn test_merge_cells_in_output() {
824        let mut sw = StreamWriter::new("Sheet1");
825        sw.write_row(1, &[CellValue::from("Merged")]).unwrap();
826        sw.add_merge_cell("A1:B1").unwrap();
827        let xml_bytes = sw.finish().unwrap();
828        let xml_str = String::from_utf8(xml_bytes).unwrap();
829
830        let ws: WorksheetXml = quick_xml::de::from_str(&xml_str).unwrap();
831        let mc = ws.merge_cells.unwrap();
832        assert_eq!(mc.merge_cells.len(), 1);
833        assert_eq!(mc.merge_cells[0].reference, "A1:B1");
834    }
835
836    #[test]
837    fn test_multiple_merge_cells() {
838        let mut sw = StreamWriter::new("Sheet1");
839        sw.write_row(1, &[CellValue::from("a")]).unwrap();
840        sw.add_merge_cell("A1:B1").unwrap();
841        sw.add_merge_cell("C1:D1").unwrap();
842        let xml_bytes = sw.finish().unwrap();
843        let xml_str = String::from_utf8(xml_bytes).unwrap();
844
845        let ws: WorksheetXml = quick_xml::de::from_str(&xml_str).unwrap();
846        let mc = ws.merge_cells.unwrap();
847        assert_eq!(mc.merge_cells.len(), 2);
848        assert_eq!(mc.merge_cells[0].reference, "A1:B1");
849        assert_eq!(mc.merge_cells[1].reference, "C1:D1");
850    }
851
852    #[test]
853    fn test_finish_twice_returns_error() {
854        let mut sw = StreamWriter::new("Sheet1");
855        sw.write_row(1, &[CellValue::from("a")]).unwrap();
856        sw.finish().unwrap();
857        let result = sw.finish();
858        assert!(result.is_err());
859        assert!(matches!(result.unwrap_err(), Error::StreamAlreadyFinished));
860    }
861
862    #[test]
863    fn test_write_after_finish_returns_error() {
864        let mut sw = StreamWriter::new("Sheet1");
865        sw.write_row(1, &[CellValue::from("a")]).unwrap();
866        sw.finish().unwrap();
867        let result = sw.write_row(2, &[CellValue::from("b")]);
868        assert!(result.is_err());
869        assert!(matches!(result.unwrap_err(), Error::StreamAlreadyFinished));
870    }
871
872    #[test]
873    fn test_finish_with_no_rows() {
874        let mut sw = StreamWriter::new("Sheet1");
875        let xml_bytes = sw.finish().unwrap();
876        let xml_str = String::from_utf8(xml_bytes).unwrap();
877
878        // Should still produce valid XML
879        let ws: WorksheetXml = quick_xml::de::from_str(&xml_str).unwrap();
880        assert!(ws.sheet_data.rows.is_empty());
881    }
882
883    #[test]
884    fn test_finish_with_cols_and_no_rows() {
885        let mut sw = StreamWriter::new("Sheet1");
886        sw.set_col_width(1, 20.0).unwrap();
887        let xml_bytes = sw.finish().unwrap();
888        let xml_str = String::from_utf8(xml_bytes).unwrap();
889
890        let ws: WorksheetXml = quick_xml::de::from_str(&xml_str).unwrap();
891        assert!(ws.cols.is_some());
892        assert!(ws.sheet_data.rows.is_empty());
893    }
894
895    #[test]
896    fn test_into_parts() {
897        let mut sw = StreamWriter::new("Sheet1");
898        sw.write_row(1, &[CellValue::from("Hello")]).unwrap();
899        let (xml_bytes, sst) = sw.into_parts().unwrap();
900
901        assert!(!xml_bytes.is_empty());
902        assert_eq!(sst.len(), 1);
903        assert_eq!(sst.get(0), Some("Hello"));
904    }
905
906    #[test]
907    fn test_sst_deduplication() {
908        let mut sw = StreamWriter::new("Sheet1");
909        sw.write_row(1, &[CellValue::from("same"), CellValue::from("same")])
910            .unwrap();
911        sw.write_row(2, &[CellValue::from("same"), CellValue::from("other")])
912            .unwrap();
913
914        assert_eq!(sw.sst().len(), 2); // "same" and "other"
915        assert_eq!(sw.sst().get(0), Some("same"));
916        assert_eq!(sw.sst().get(1), Some("other"));
917    }
918
919    #[test]
920    fn test_large_scale_10000_rows() {
921        let mut sw = StreamWriter::new("BigSheet");
922        for i in 1..=10_000u32 {
923            sw.write_row(
924                i,
925                &[
926                    CellValue::from(format!("Row {}", i)),
927                    CellValue::from(i as i32),
928                    CellValue::from(i as f64 * 1.5),
929                ],
930            )
931            .unwrap();
932        }
933        let xml_bytes = sw.finish().unwrap();
934        let xml_str = String::from_utf8(xml_bytes).unwrap();
935
936        // Verify it parses
937        let ws: WorksheetXml = quick_xml::de::from_str(&xml_str).unwrap();
938        assert_eq!(ws.sheet_data.rows.len(), 10_000);
939        assert_eq!(ws.sheet_data.rows[0].r, 1);
940        assert_eq!(ws.sheet_data.rows[9999].r, 10_000);
941    }
942
943    #[test]
944    fn test_large_sst_dedup() {
945        let mut sw = StreamWriter::new("Sheet1");
946        // Write 1000 rows, each with the same 3 string values
947        for i in 1..=1000u32 {
948            sw.write_row(
949                i,
950                &[
951                    CellValue::from("Alpha"),
952                    CellValue::from("Beta"),
953                    CellValue::from("Gamma"),
954                ],
955            )
956            .unwrap();
957        }
958        // SST should only have 3 unique strings
959        assert_eq!(sw.sst().len(), 3);
960    }
961
962    #[test]
963    fn test_xml_escape_in_formula() {
964        let mut sw = StreamWriter::new("Sheet1");
965        sw.write_row(
966            1,
967            &[CellValue::Formula {
968                expr: "IF(A1>0,\"yes\",\"no\")".to_string(),
969                result: None,
970            }],
971        )
972        .unwrap();
973        let xml_bytes = sw.finish().unwrap();
974        let xml = String::from_utf8(xml_bytes).unwrap();
975        // quick_xml escapes > in text content; quotes don't require escaping in text content
976        assert!(xml.contains("&gt;"));
977        // Verify the formula round-trips through XML
978        let ws: WorksheetXml = quick_xml::de::from_str(&xml).unwrap();
979        let formula = ws.sheet_data.rows[0].cells[0].f.as_ref().unwrap();
980        assert_eq!(formula.value, Some("IF(A1>0,\"yes\",\"no\")".to_string()));
981    }
982
983    #[test]
984    fn test_add_merge_cell_invalid_reference() {
985        let mut sw = StreamWriter::new("Sheet1");
986        // Missing colon separator.
987        let result = sw.add_merge_cell("A1B2");
988        assert!(result.is_err());
989        assert!(matches!(
990            result.unwrap_err(),
991            Error::InvalidMergeCellReference(_)
992        ));
993    }
994
995    #[test]
996    fn test_add_merge_cell_invalid_cell_name() {
997        let mut sw = StreamWriter::new("Sheet1");
998        // Invalid cell name before colon.
999        let result = sw.add_merge_cell("ZZZ:B2");
1000        assert!(result.is_err());
1001        assert!(matches!(
1002            result.unwrap_err(),
1003            Error::InvalidMergeCellReference(_)
1004        ));
1005    }
1006
1007    #[test]
1008    fn test_add_merge_cell_empty_reference() {
1009        let mut sw = StreamWriter::new("Sheet1");
1010        let result = sw.add_merge_cell("");
1011        assert!(result.is_err());
1012        assert!(matches!(
1013            result.unwrap_err(),
1014            Error::InvalidMergeCellReference(_)
1015        ));
1016    }
1017
1018    #[test]
1019    fn test_add_merge_cell_after_finish_fails() {
1020        let mut sw = StreamWriter::new("Sheet1");
1021        sw.finish().unwrap();
1022        let result = sw.add_merge_cell("A1:B1");
1023        assert!(result.is_err());
1024        assert!(matches!(result.unwrap_err(), Error::StreamAlreadyFinished));
1025    }
1026
1027    #[test]
1028    fn test_set_col_width_after_finish_fails() {
1029        let mut sw = StreamWriter::new("Sheet1");
1030        sw.finish().unwrap();
1031        let result = sw.set_col_width(1, 10.0);
1032        assert!(result.is_err());
1033        assert!(matches!(result.unwrap_err(), Error::StreamAlreadyFinished));
1034    }
1035
1036    #[test]
1037    fn test_sheet_name_getter() {
1038        let sw = StreamWriter::new("MySheet");
1039        assert_eq!(sw.sheet_name(), "MySheet");
1040    }
1041
1042    #[test]
1043    fn test_all_value_types_in_single_row() {
1044        let mut sw = StreamWriter::new("Sheet1");
1045        sw.write_row(
1046            1,
1047            &[
1048                CellValue::from("text"),
1049                CellValue::from(42),
1050                CellValue::from(3.15),
1051                CellValue::from(true),
1052                CellValue::from(false),
1053                CellValue::Formula {
1054                    expr: "A1+B1".to_string(),
1055                    result: None,
1056                },
1057                CellValue::Error("#N/A".to_string()),
1058                CellValue::Empty,
1059            ],
1060        )
1061        .unwrap();
1062        let xml_bytes = sw.finish().unwrap();
1063        let xml_str = String::from_utf8(xml_bytes).unwrap();
1064
1065        // Should parse without error
1066        let ws: WorksheetXml = quick_xml::de::from_str(&xml_str).unwrap();
1067        // 7 cells (Empty is skipped)
1068        assert_eq!(ws.sheet_data.rows[0].cells.len(), 7);
1069    }
1070
1071    #[test]
1072    fn test_col_width_invalid_column_zero() {
1073        let mut sw = StreamWriter::new("Sheet1");
1074        let result = sw.set_col_width(0, 10.0);
1075        assert!(result.is_err());
1076        assert!(matches!(result.unwrap_err(), Error::InvalidColumnNumber(0)));
1077    }
1078
1079    #[test]
1080    fn test_write_row_with_options_height() {
1081        let mut sw = StreamWriter::new("Sheet1");
1082        let opts = StreamRowOptions {
1083            height: Some(30.0),
1084            ..Default::default()
1085        };
1086        sw.write_row_with_options(1, &[CellValue::from("tall row")], &opts)
1087            .unwrap();
1088        let xml_bytes = sw.finish().unwrap();
1089        let xml = String::from_utf8(xml_bytes).unwrap();
1090
1091        // Parse back and verify
1092        let ws: WorksheetXml = quick_xml::de::from_str(&xml).unwrap();
1093        assert_eq!(ws.sheet_data.rows[0].ht, Some(30.0));
1094        assert_eq!(ws.sheet_data.rows[0].custom_height, Some(true));
1095    }
1096
1097    #[test]
1098    fn test_write_row_with_options_hidden() {
1099        let mut sw = StreamWriter::new("Sheet1");
1100        let opts = StreamRowOptions {
1101            visible: Some(false),
1102            ..Default::default()
1103        };
1104        sw.write_row_with_options(1, &[CellValue::from("hidden")], &opts)
1105            .unwrap();
1106        let xml_bytes = sw.finish().unwrap();
1107        let xml = String::from_utf8(xml_bytes).unwrap();
1108
1109        let ws: WorksheetXml = quick_xml::de::from_str(&xml).unwrap();
1110        assert_eq!(ws.sheet_data.rows[0].hidden, Some(true));
1111    }
1112
1113    #[test]
1114    fn test_write_row_with_options_outline_level() {
1115        let mut sw = StreamWriter::new("Sheet1");
1116        let opts = StreamRowOptions {
1117            outline_level: Some(3),
1118            ..Default::default()
1119        };
1120        sw.write_row_with_options(1, &[CellValue::from("grouped")], &opts)
1121            .unwrap();
1122        let xml_bytes = sw.finish().unwrap();
1123        let xml = String::from_utf8(xml_bytes).unwrap();
1124
1125        let ws: WorksheetXml = quick_xml::de::from_str(&xml).unwrap();
1126        assert_eq!(ws.sheet_data.rows[0].outline_level, Some(3));
1127    }
1128
1129    #[test]
1130    fn test_write_row_with_options_style() {
1131        let mut sw = StreamWriter::new("Sheet1");
1132        let opts = StreamRowOptions {
1133            style_id: Some(2),
1134            ..Default::default()
1135        };
1136        sw.write_row_with_options(1, &[CellValue::from("styled row")], &opts)
1137            .unwrap();
1138        let xml_bytes = sw.finish().unwrap();
1139        let xml = String::from_utf8(xml_bytes).unwrap();
1140
1141        let ws: WorksheetXml = quick_xml::de::from_str(&xml).unwrap();
1142        assert_eq!(ws.sheet_data.rows[0].s, Some(2));
1143        assert_eq!(ws.sheet_data.rows[0].custom_format, Some(true));
1144    }
1145
1146    #[test]
1147    fn test_write_row_with_options_all() {
1148        let mut sw = StreamWriter::new("Sheet1");
1149        let opts = StreamRowOptions {
1150            height: Some(25.5),
1151            visible: Some(false),
1152            outline_level: Some(2),
1153            style_id: Some(7),
1154        };
1155        sw.write_row_with_options(1, &[CellValue::from("all options")], &opts)
1156            .unwrap();
1157        let xml_bytes = sw.finish().unwrap();
1158        let xml = String::from_utf8(xml_bytes).unwrap();
1159
1160        let ws: WorksheetXml = quick_xml::de::from_str(&xml).unwrap();
1161        let row = &ws.sheet_data.rows[0];
1162        assert_eq!(row.ht, Some(25.5));
1163        assert_eq!(row.custom_height, Some(true));
1164        assert_eq!(row.hidden, Some(true));
1165        assert_eq!(row.outline_level, Some(2));
1166        assert_eq!(row.s, Some(7));
1167        assert_eq!(row.custom_format, Some(true));
1168    }
1169
1170    #[test]
1171    fn test_write_row_with_options_height_exceeded() {
1172        let mut sw = StreamWriter::new("Sheet1");
1173        let opts = StreamRowOptions {
1174            height: Some(500.0),
1175            ..Default::default()
1176        };
1177        let result = sw.write_row_with_options(1, &[CellValue::from("too tall")], &opts);
1178        assert!(result.is_err());
1179        assert!(matches!(
1180            result.unwrap_err(),
1181            Error::RowHeightExceeded { .. }
1182        ));
1183    }
1184
1185    #[test]
1186    fn test_write_row_with_options_outline_level_exceeded() {
1187        let mut sw = StreamWriter::new("Sheet1");
1188        let opts = StreamRowOptions {
1189            outline_level: Some(8),
1190            ..Default::default()
1191        };
1192        let result = sw.write_row_with_options(1, &[CellValue::from("bad level")], &opts);
1193        assert!(result.is_err());
1194    }
1195
1196    #[test]
1197    fn test_write_row_with_options_visible_true_no_hidden_attr() {
1198        let mut sw = StreamWriter::new("Sheet1");
1199        let opts = StreamRowOptions {
1200            visible: Some(true),
1201            ..Default::default()
1202        };
1203        sw.write_row_with_options(1, &[CellValue::from("visible")], &opts)
1204            .unwrap();
1205        let xml_bytes = sw.finish().unwrap();
1206        let xml = String::from_utf8(xml_bytes).unwrap();
1207
1208        // visible=true should NOT produce hidden attribute
1209        assert!(!xml.contains("hidden="));
1210    }
1211
1212    #[test]
1213    fn test_col_style() {
1214        let mut sw = StreamWriter::new("Sheet1");
1215        sw.set_col_style(1, 3).unwrap();
1216        sw.write_row(1, &[CellValue::from("data")]).unwrap();
1217        let xml_bytes = sw.finish().unwrap();
1218        let xml = String::from_utf8(xml_bytes).unwrap();
1219
1220        let ws: WorksheetXml = quick_xml::de::from_str(&xml).unwrap();
1221        let cols = ws.cols.unwrap();
1222        let styled_col = cols.cols.iter().find(|c| c.style.is_some()).unwrap();
1223        assert_eq!(styled_col.style, Some(3));
1224        assert_eq!(styled_col.min, 1);
1225        assert_eq!(styled_col.max, 1);
1226    }
1227
1228    #[test]
1229    fn test_col_visible() {
1230        let mut sw = StreamWriter::new("Sheet1");
1231        sw.set_col_visible(2, false).unwrap();
1232        sw.write_row(1, &[CellValue::from("a"), CellValue::from("b")])
1233            .unwrap();
1234        let xml_bytes = sw.finish().unwrap();
1235        let xml = String::from_utf8(xml_bytes).unwrap();
1236
1237        let ws: WorksheetXml = quick_xml::de::from_str(&xml).unwrap();
1238        let cols = ws.cols.unwrap();
1239        let hidden_col = cols.cols.iter().find(|c| c.hidden == Some(true)).unwrap();
1240        assert_eq!(hidden_col.min, 2);
1241        assert_eq!(hidden_col.max, 2);
1242    }
1243
1244    #[test]
1245    fn test_col_outline_level() {
1246        let mut sw = StreamWriter::new("Sheet1");
1247        sw.set_col_outline_level(3, 2).unwrap();
1248        sw.write_row(1, &[CellValue::from("a")]).unwrap();
1249        let xml_bytes = sw.finish().unwrap();
1250        let xml = String::from_utf8(xml_bytes).unwrap();
1251
1252        let ws: WorksheetXml = quick_xml::de::from_str(&xml).unwrap();
1253        let cols = ws.cols.unwrap();
1254        let outlined_col = cols
1255            .cols
1256            .iter()
1257            .find(|c| c.outline_level.is_some())
1258            .unwrap();
1259        assert_eq!(outlined_col.outline_level, Some(2));
1260        assert_eq!(outlined_col.min, 3);
1261        assert_eq!(outlined_col.max, 3);
1262    }
1263
1264    #[test]
1265    fn test_col_outline_level_exceeded() {
1266        let mut sw = StreamWriter::new("Sheet1");
1267        let result = sw.set_col_outline_level(1, 8);
1268        assert!(result.is_err());
1269    }
1270
1271    #[test]
1272    fn test_col_style_after_rows_error() {
1273        let mut sw = StreamWriter::new("Sheet1");
1274        sw.write_row(1, &[CellValue::from("data")]).unwrap();
1275        let result = sw.set_col_style(1, 1);
1276        assert!(result.is_err());
1277        assert!(matches!(result.unwrap_err(), Error::StreamColumnsAfterRows));
1278    }
1279
1280    #[test]
1281    fn test_col_visible_after_rows_error() {
1282        let mut sw = StreamWriter::new("Sheet1");
1283        sw.write_row(1, &[CellValue::from("data")]).unwrap();
1284        let result = sw.set_col_visible(1, false);
1285        assert!(result.is_err());
1286        assert!(matches!(result.unwrap_err(), Error::StreamColumnsAfterRows));
1287    }
1288
1289    #[test]
1290    fn test_col_outline_after_rows_error() {
1291        let mut sw = StreamWriter::new("Sheet1");
1292        sw.write_row(1, &[CellValue::from("data")]).unwrap();
1293        let result = sw.set_col_outline_level(1, 1);
1294        assert!(result.is_err());
1295        assert!(matches!(result.unwrap_err(), Error::StreamColumnsAfterRows));
1296    }
1297
1298    #[test]
1299    fn test_freeze_panes_rows() {
1300        let mut sw = StreamWriter::new("Sheet1");
1301        sw.set_freeze_panes("A2").unwrap();
1302        sw.write_row(1, &[CellValue::from("header")]).unwrap();
1303        sw.write_row(2, &[CellValue::from("data")]).unwrap();
1304        let xml_bytes = sw.finish().unwrap();
1305        let xml_str = String::from_utf8(xml_bytes).unwrap();
1306
1307        let ws: WorksheetXml = quick_xml::de::from_str(&xml_str).unwrap();
1308        let views = ws.sheet_views.unwrap();
1309        let pane = views.sheet_views[0].pane.as_ref().unwrap();
1310        assert_eq!(pane.y_split, Some(1));
1311        assert_eq!(pane.top_left_cell, Some("A2".to_string()));
1312        assert_eq!(pane.active_pane, Some("bottomLeft".to_string()));
1313        assert_eq!(pane.state, Some("frozen".to_string()));
1314        // xSplit should not appear when only rows are frozen
1315        assert_eq!(pane.x_split, None);
1316    }
1317
1318    #[test]
1319    fn test_freeze_panes_cols() {
1320        let mut sw = StreamWriter::new("Sheet1");
1321        sw.set_freeze_panes("B1").unwrap();
1322        sw.write_row(1, &[CellValue::from("a"), CellValue::from("b")])
1323            .unwrap();
1324        let xml_bytes = sw.finish().unwrap();
1325        let xml_str = String::from_utf8(xml_bytes).unwrap();
1326
1327        let ws: WorksheetXml = quick_xml::de::from_str(&xml_str).unwrap();
1328        let views = ws.sheet_views.unwrap();
1329        let pane = views.sheet_views[0].pane.as_ref().unwrap();
1330        assert_eq!(pane.x_split, Some(1));
1331        assert_eq!(pane.top_left_cell, Some("B1".to_string()));
1332        assert_eq!(pane.active_pane, Some("topRight".to_string()));
1333        assert_eq!(pane.state, Some("frozen".to_string()));
1334        // ySplit should not appear when only cols are frozen
1335        assert_eq!(pane.y_split, None);
1336    }
1337
1338    #[test]
1339    fn test_freeze_panes_both() {
1340        let mut sw = StreamWriter::new("Sheet1");
1341        sw.set_freeze_panes("C3").unwrap();
1342        sw.write_row(1, &[CellValue::from("a")]).unwrap();
1343        let xml_bytes = sw.finish().unwrap();
1344        let xml_str = String::from_utf8(xml_bytes).unwrap();
1345
1346        let ws: WorksheetXml = quick_xml::de::from_str(&xml_str).unwrap();
1347        let views = ws.sheet_views.unwrap();
1348        let pane = views.sheet_views[0].pane.as_ref().unwrap();
1349        assert_eq!(pane.x_split, Some(2));
1350        assert_eq!(pane.y_split, Some(2));
1351        assert_eq!(pane.top_left_cell, Some("C3".to_string()));
1352        assert_eq!(pane.active_pane, Some("bottomRight".to_string()));
1353        assert_eq!(pane.state, Some("frozen".to_string()));
1354    }
1355
1356    #[test]
1357    fn test_freeze_panes_after_rows_error() {
1358        let mut sw = StreamWriter::new("Sheet1");
1359        sw.write_row(1, &[CellValue::from("data")]).unwrap();
1360        let result = sw.set_freeze_panes("A2");
1361        assert!(result.is_err());
1362        assert!(matches!(result.unwrap_err(), Error::StreamColumnsAfterRows));
1363    }
1364
1365    #[test]
1366    fn test_freeze_panes_a1_error() {
1367        let mut sw = StreamWriter::new("Sheet1");
1368        let result = sw.set_freeze_panes("A1");
1369        assert!(result.is_err());
1370        assert!(matches!(
1371            result.unwrap_err(),
1372            Error::InvalidCellReference(_)
1373        ));
1374    }
1375
1376    #[test]
1377    fn test_freeze_panes_invalid_cell_error() {
1378        let mut sw = StreamWriter::new("Sheet1");
1379        let result = sw.set_freeze_panes("ZZZZ1");
1380        assert!(result.is_err());
1381    }
1382
1383    #[test]
1384    fn test_freeze_panes_appears_before_cols() {
1385        let mut sw = StreamWriter::new("Sheet1");
1386        sw.set_freeze_panes("A2").unwrap();
1387        sw.set_col_width(1, 20.0).unwrap();
1388        sw.write_row(1, &[CellValue::from("data")]).unwrap();
1389        let xml_bytes = sw.finish().unwrap();
1390        let xml = String::from_utf8(xml_bytes).unwrap();
1391
1392        let views_pos = xml.find("<sheetView").unwrap();
1393        let cols_pos = xml.find("<cols>").unwrap();
1394        let data_pos = xml.find("<sheetData").unwrap();
1395        assert!(views_pos < cols_pos);
1396        assert!(cols_pos < data_pos);
1397    }
1398
1399    #[test]
1400    fn test_freeze_panes_after_finish_error() {
1401        let mut sw = StreamWriter::new("Sheet1");
1402        sw.finish().unwrap();
1403        let result = sw.set_freeze_panes("A2");
1404        assert!(result.is_err());
1405        assert!(matches!(result.unwrap_err(), Error::StreamAlreadyFinished));
1406    }
1407
1408    #[test]
1409    fn test_no_freeze_panes_no_sheet_views() {
1410        let mut sw = StreamWriter::new("Sheet1");
1411        sw.write_row(1, &[CellValue::from("data")]).unwrap();
1412        let xml_bytes = sw.finish().unwrap();
1413        let xml = String::from_utf8(xml_bytes).unwrap();
1414
1415        // When no freeze panes are set, sheetViews should not appear
1416        assert!(!xml.contains("<sheetView"));
1417    }
1418
1419    #[test]
1420    fn test_write_row_backward_compat() {
1421        // Ensure the original write_row still works exactly as before.
1422        let mut sw = StreamWriter::new("Sheet1");
1423        sw.write_row(1, &[CellValue::from("hello")]).unwrap();
1424        let xml_bytes = sw.finish().unwrap();
1425        let xml = String::from_utf8(xml_bytes).unwrap();
1426
1427        let ws: WorksheetXml = quick_xml::de::from_str(&xml).unwrap();
1428        let row = &ws.sheet_data.rows[0];
1429        assert_eq!(row.r, 1);
1430        // Should not have any row-level attributes beyond r
1431        assert!(row.ht.is_none());
1432        assert!(row.hidden.is_none());
1433        assert!(row.outline_level.is_none());
1434        assert!(row.custom_format.is_none());
1435    }
1436
1437    #[test]
1438    fn test_write_row_with_style_backward_compat() {
1439        // Ensure the original write_row_with_style still works.
1440        let mut sw = StreamWriter::new("Sheet1");
1441        sw.write_row_with_style(1, &[CellValue::from("styled")], 5)
1442            .unwrap();
1443        let xml_bytes = sw.finish().unwrap();
1444        let xml = String::from_utf8(xml_bytes).unwrap();
1445
1446        // Cell should have s="5" attribute
1447        assert!(xml.contains("s=\"5\""));
1448        // Row should not have row-level style attributes
1449        let ws: WorksheetXml = quick_xml::de::from_str(&xml).unwrap();
1450        assert!(ws.sheet_data.rows[0].custom_format.is_none());
1451    }
1452
1453    #[test]
1454    fn test_col_options_combined_with_widths() {
1455        let mut sw = StreamWriter::new("Sheet1");
1456        sw.set_col_width(1, 20.0).unwrap();
1457        sw.set_col_style(2, 5).unwrap();
1458        sw.set_col_visible(3, false).unwrap();
1459        sw.write_row(
1460            1,
1461            &[
1462                CellValue::from("a"),
1463                CellValue::from("b"),
1464                CellValue::from("c"),
1465            ],
1466        )
1467        .unwrap();
1468        let xml_bytes = sw.finish().unwrap();
1469        let xml = String::from_utf8(xml_bytes).unwrap();
1470
1471        let ws: WorksheetXml = quick_xml::de::from_str(&xml).unwrap();
1472        let cols = ws.cols.unwrap();
1473        assert_eq!(cols.cols.len(), 3);
1474    }
1475
1476    #[test]
1477    fn test_into_worksheet_parts_basic() {
1478        let mut sw = StreamWriter::new("Sheet1");
1479        sw.write_row(1, &[CellValue::from("Hello"), CellValue::from(42)])
1480            .unwrap();
1481        sw.write_row(2, &[CellValue::from("World"), CellValue::from(99)])
1482            .unwrap();
1483        let (ws, sst) = sw.into_worksheet_parts().unwrap();
1484        assert_eq!(ws.sheet_data.rows.len(), 2);
1485        assert_eq!(ws.sheet_data.rows[0].r, 1);
1486        assert_eq!(ws.sheet_data.rows[0].cells.len(), 2);
1487        assert_eq!(ws.sheet_data.rows[1].r, 2);
1488        assert_eq!(sst.len(), 2);
1489    }
1490
1491    #[test]
1492    fn test_into_worksheet_parts_sst() {
1493        let mut sw = StreamWriter::new("Sheet1");
1494        sw.write_row(1, &[CellValue::from("Alpha"), CellValue::from("Beta")])
1495            .unwrap();
1496        let (ws, sst) = sw.into_worksheet_parts().unwrap();
1497        assert_eq!(sst.len(), 2);
1498        assert_eq!(sst.get(0), Some("Alpha"));
1499        assert_eq!(sst.get(1), Some("Beta"));
1500        // Cells should reference SST indices
1501        assert_eq!(ws.sheet_data.rows[0].cells[0].v, Some("0".to_string()));
1502        assert_eq!(ws.sheet_data.rows[0].cells[1].v, Some("1".to_string()));
1503        assert_eq!(
1504            ws.sheet_data.rows[0].cells[0].t,
1505            sheetkit_xml::worksheet::CellTypeTag::SharedString
1506        );
1507    }
1508
1509    #[test]
1510    fn test_into_worksheet_parts_cell_col_set() {
1511        let mut sw = StreamWriter::new("Sheet1");
1512        sw.write_row(
1513            1,
1514            &[CellValue::from("A"), CellValue::Empty, CellValue::from("C")],
1515        )
1516        .unwrap();
1517        let (ws, _) = sw.into_worksheet_parts().unwrap();
1518        assert_eq!(ws.sheet_data.rows[0].cells.len(), 2);
1519        assert_eq!(ws.sheet_data.rows[0].cells[0].col, 1);
1520        assert_eq!(ws.sheet_data.rows[0].cells[0].r.as_str(), "A1");
1521        assert_eq!(ws.sheet_data.rows[0].cells[1].col, 3);
1522        assert_eq!(ws.sheet_data.rows[0].cells[1].r.as_str(), "C1");
1523    }
1524
1525    #[test]
1526    fn test_into_worksheet_parts_freeze_panes() {
1527        let mut sw = StreamWriter::new("Sheet1");
1528        sw.set_freeze_panes("C3").unwrap();
1529        sw.write_row(1, &[CellValue::from("data")]).unwrap();
1530        let (ws, _) = sw.into_worksheet_parts().unwrap();
1531        assert!(ws.sheet_views.is_some());
1532        let views = ws.sheet_views.unwrap();
1533        let pane = views.sheet_views[0].pane.as_ref().unwrap();
1534        assert_eq!(pane.x_split, Some(2));
1535        assert_eq!(pane.y_split, Some(2));
1536        assert_eq!(pane.top_left_cell, Some("C3".to_string()));
1537        assert_eq!(pane.state, Some("frozen".to_string()));
1538    }
1539
1540    #[test]
1541    fn test_into_worksheet_parts_cols() {
1542        let mut sw = StreamWriter::new("Sheet1");
1543        sw.set_col_width(1, 20.0).unwrap();
1544        sw.set_col_style(2, 5).unwrap();
1545        sw.write_row(1, &[CellValue::from("data")]).unwrap();
1546        let (ws, _) = sw.into_worksheet_parts().unwrap();
1547        assert!(ws.cols.is_some());
1548        let cols = ws.cols.unwrap();
1549        assert_eq!(cols.cols.len(), 2);
1550    }
1551
1552    #[test]
1553    fn test_into_worksheet_parts_merge_cells() {
1554        let mut sw = StreamWriter::new("Sheet1");
1555        sw.write_row(1, &[CellValue::from("Merged")]).unwrap();
1556        sw.add_merge_cell("A1:B1").unwrap();
1557        let (ws, _) = sw.into_worksheet_parts().unwrap();
1558        assert!(ws.merge_cells.is_some());
1559        let mc = ws.merge_cells.unwrap();
1560        assert_eq!(mc.merge_cells.len(), 1);
1561        assert_eq!(mc.merge_cells[0].reference, "A1:B1");
1562    }
1563}