Skip to main content

sheetkit_core/
stream.rs

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