sheetkit_core/
stream.rs

1//! Streaming worksheet writer.
2//!
3//! The [`StreamWriter`] writes row data directly to a temporary file instead of
4//! accumulating it in memory. This enables writing sheets with millions of rows
5//! without holding the entire worksheet in memory.
6//!
7//! Strings are written as inline strings (`<is><t>...</t></is>`) rather than
8//! shared string references, eliminating the need for SST index remapping and
9//! allowing each row to be serialized independently.
10//!
11//! Rows must be written in ascending order.
12//!
13//! # Example
14//!
15//! ```
16//! use sheetkit_core::stream::StreamWriter;
17//! use sheetkit_core::cell::CellValue;
18//!
19//! let mut sw = StreamWriter::new("Sheet1");
20//! sw.set_col_width(1, 20.0).unwrap();
21//! sw.write_row(1, &[CellValue::from("Name"), CellValue::from("Age")]).unwrap();
22//! sw.write_row(2, &[CellValue::from("Alice"), CellValue::from(30)]).unwrap();
23//! // StreamWriter data is applied to a workbook via apply_stream_writer().
24//! ```
25
26use std::io::{BufWriter, Write as _};
27
28use crate::cell::CellValue;
29use crate::error::{Error, Result};
30use crate::utils::cell_ref::cell_name_to_coordinates;
31use crate::utils::constants::{MAX_COLUMNS, MAX_ROWS, MAX_ROW_HEIGHT};
32
33use sheetkit_xml::worksheet::{Col, Cols, MergeCell, MergeCells, Pane, SheetView, SheetViews};
34
35/// Maximum outline level allowed by Excel.
36const MAX_OUTLINE_LEVEL: u8 = 7;
37
38/// Options for a streamed row.
39#[derive(Debug, Clone, Default)]
40pub struct StreamRowOptions {
41    /// Custom row height in points.
42    pub height: Option<f64>,
43    /// Row visibility (false = hidden).
44    pub visible: Option<bool>,
45    /// Outline level (0-7).
46    pub outline_level: Option<u8>,
47    /// Style ID for the row.
48    pub style_id: Option<u32>,
49}
50
51/// Internal column configuration beyond width.
52#[derive(Debug, Clone, Default)]
53struct StreamColOptions {
54    width: Option<f64>,
55    style_id: Option<u32>,
56    hidden: Option<bool>,
57    outline_level: Option<u8>,
58}
59
60/// Metadata for a streamed sheet stored in the workbook after applying.
61///
62/// Contains the temporary file with pre-serialized row XML and the
63/// configuration needed to compose the full worksheet XML on save.
64pub struct StreamedSheetData {
65    /// Temporary file containing `<row>...</row>` XML fragments.
66    pub(crate) temp_file: tempfile::NamedTempFile,
67    /// Number of bytes written to the temp file (used for diagnostics/testing).
68    #[allow(dead_code)]
69    pub(crate) data_len: u64,
70    /// Pre-built `<sheetViews>` XML fragment (if freeze panes are set).
71    pub(crate) sheet_views_xml: Option<String>,
72    /// Pre-built `<cols>` XML fragment (if column settings exist).
73    pub(crate) cols_xml: Option<String>,
74    /// Pre-built `<mergeCells>` XML fragment (if merge cells exist).
75    pub(crate) merge_cells_xml: Option<String>,
76}
77
78impl StreamedSheetData {
79    /// Create a deep copy of this streamed sheet data, duplicating the
80    /// temp file contents so the copy is fully independent.
81    pub(crate) fn try_clone(&self) -> Result<Self> {
82        let mut new_temp = tempfile::NamedTempFile::new()?;
83        let mut src = std::fs::File::open(self.temp_file.path())?;
84        std::io::copy(&mut src, new_temp.as_file_mut())?;
85        Ok(StreamedSheetData {
86            temp_file: new_temp,
87            data_len: self.data_len,
88            sheet_views_xml: self.sheet_views_xml.clone(),
89            cols_xml: self.cols_xml.clone(),
90            merge_cells_xml: self.merge_cells_xml.clone(),
91        })
92    }
93}
94
95/// A streaming worksheet writer that writes rows directly to a temp file.
96///
97/// Rows must be written in ascending order. Each row is serialized to XML
98/// and written to a temporary file immediately, keeping memory usage constant
99/// regardless of the number of rows.
100pub struct StreamWriter {
101    sheet_name: String,
102    writer: BufWriter<tempfile::NamedTempFile>,
103    bytes_written: u64,
104    last_row: u32,
105    started: bool,
106    finished: bool,
107    col_widths: Vec<(u32, u32, f64)>,
108    col_options: Vec<(u32, StreamColOptions)>,
109    merge_cells: Vec<String>,
110    /// Freeze pane: (x_split, y_split, top_left_cell).
111    freeze_pane: Option<(u32, u32, String)>,
112}
113
114// StreamWriter contains a BufWriter<NamedTempFile> which is not Send by
115// default on all platforms. The temp file is exclusively owned by the writer
116// so this is safe.
117unsafe impl Send for StreamWriter {}
118
119impl std::fmt::Debug for StreamWriter {
120    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121        f.debug_struct("StreamWriter")
122            .field("sheet_name", &self.sheet_name)
123            .field("bytes_written", &self.bytes_written)
124            .field("last_row", &self.last_row)
125            .field("started", &self.started)
126            .field("finished", &self.finished)
127            .finish()
128    }
129}
130
131impl StreamWriter {
132    /// Create a new StreamWriter for the given sheet name.
133    pub fn new(sheet_name: &str) -> Self {
134        let temp = tempfile::NamedTempFile::new().expect("failed to create temp file");
135        Self {
136            sheet_name: sheet_name.to_string(),
137            writer: BufWriter::new(temp),
138            bytes_written: 0,
139            last_row: 0,
140            started: false,
141            finished: false,
142            col_widths: Vec::new(),
143            col_options: Vec::new(),
144            merge_cells: Vec::new(),
145            freeze_pane: None,
146        }
147    }
148
149    /// Get the sheet name.
150    pub fn sheet_name(&self) -> &str {
151        &self.sheet_name
152    }
153
154    /// Set column width for a single column (1-based).
155    /// Must be called before any write_row() calls.
156    pub fn set_col_width(&mut self, col: u32, width: f64) -> Result<()> {
157        self.set_col_width_range(col, col, width)
158    }
159
160    /// Set column width for a range (min_col..=max_col), both 1-based.
161    /// Must be called before any write_row() calls.
162    pub fn set_col_width_range(&mut self, min_col: u32, max_col: u32, width: f64) -> Result<()> {
163        if self.finished {
164            return Err(Error::StreamAlreadyFinished);
165        }
166        if self.started {
167            return Err(Error::StreamColumnsAfterRows);
168        }
169        if min_col == 0 || max_col == 0 || min_col > MAX_COLUMNS || max_col > MAX_COLUMNS {
170            return Err(Error::InvalidColumnNumber(if min_col == 0 {
171                min_col
172            } else {
173                max_col
174            }));
175        }
176        self.col_widths.push((min_col, max_col, width));
177        Ok(())
178    }
179
180    /// Set column style for a single column (1-based).
181    /// Must be called before any write_row() calls.
182    pub fn set_col_style(&mut self, col: u32, style_id: u32) -> Result<()> {
183        self.ensure_col_configurable(col)?;
184        self.get_or_create_col_options(col).style_id = Some(style_id);
185        Ok(())
186    }
187
188    /// Set column visibility for a single column (1-based).
189    /// Must be called before any write_row() calls.
190    pub fn set_col_visible(&mut self, col: u32, visible: bool) -> Result<()> {
191        self.ensure_col_configurable(col)?;
192        self.get_or_create_col_options(col).hidden = Some(!visible);
193        Ok(())
194    }
195
196    /// Set column outline level for a single column (1-based).
197    /// Must be called before any write_row() calls. Level must be 0-7.
198    pub fn set_col_outline_level(&mut self, col: u32, level: u8) -> Result<()> {
199        self.ensure_col_configurable(col)?;
200        if level > MAX_OUTLINE_LEVEL {
201            return Err(Error::OutlineLevelExceeded {
202                level,
203                max: MAX_OUTLINE_LEVEL,
204            });
205        }
206        self.get_or_create_col_options(col).outline_level = Some(level);
207        Ok(())
208    }
209
210    /// Set freeze panes for the streamed sheet.
211    /// Must be called before any write_row() calls.
212    /// `top_left_cell` is the cell below and to the right of the frozen area,
213    /// e.g., "A2" freezes row 1, "B1" freezes column A, "C3" freezes rows 1-2
214    /// and columns A-B.
215    pub fn set_freeze_panes(&mut self, top_left_cell: &str) -> Result<()> {
216        if self.finished {
217            return Err(Error::StreamAlreadyFinished);
218        }
219        if self.started {
220            return Err(Error::StreamColumnsAfterRows);
221        }
222        let (col, row) = cell_name_to_coordinates(top_left_cell)?;
223        if col == 1 && row == 1 {
224            return Err(Error::InvalidCellReference(
225                "freeze pane at A1 has no effect".to_string(),
226            ));
227        }
228        self.freeze_pane = Some((col - 1, row - 1, top_left_cell.to_string()));
229        Ok(())
230    }
231
232    /// Write a row of values with row-level options.
233    pub fn write_row_with_options(
234        &mut self,
235        row: u32,
236        values: &[CellValue],
237        options: &StreamRowOptions,
238    ) -> Result<()> {
239        self.write_row_impl(row, values, None, Some(options))
240    }
241
242    /// Write a row of values. Rows must be written in ascending order.
243    /// Row numbers are 1-based.
244    pub fn write_row(&mut self, row: u32, values: &[CellValue]) -> Result<()> {
245        self.write_row_impl(row, values, None, None)
246    }
247
248    /// Write multiple rows of values starting at the given row number.
249    /// Each element in the slice becomes a row. Rows are numbered consecutively
250    /// from `start_row`. All rows must be in ascending order relative to any
251    /// previously written row.
252    pub fn write_rows(&mut self, start_row: u32, rows: &[Vec<CellValue>]) -> Result<()> {
253        for (i, values) in rows.iter().enumerate() {
254            let row_num = start_row
255                .checked_add(i as u32)
256                .ok_or(Error::InvalidRowNumber(u32::MAX))?;
257            self.write_row_impl(row_num, values, None, None)?;
258        }
259        Ok(())
260    }
261
262    /// Write a row with a specific style ID applied to all cells.
263    pub fn write_row_with_style(
264        &mut self,
265        row: u32,
266        values: &[CellValue],
267        style_id: u32,
268    ) -> Result<()> {
269        self.write_row_impl(row, values, Some(style_id), None)
270    }
271
272    /// Add a merge cell reference (e.g., "A1:B2").
273    /// Must be called before finish(). The reference must be a valid range
274    /// in the form "A1:B2".
275    pub fn add_merge_cell(&mut self, reference: &str) -> Result<()> {
276        if self.finished {
277            return Err(Error::StreamAlreadyFinished);
278        }
279        // Validate the range format.
280        let parts: Vec<&str> = reference.split(':').collect();
281        if parts.len() != 2 {
282            return Err(Error::InvalidMergeCellReference(reference.to_string()));
283        }
284        cell_name_to_coordinates(parts[0])
285            .map_err(|_| Error::InvalidMergeCellReference(reference.to_string()))?;
286        cell_name_to_coordinates(parts[1])
287            .map_err(|_| Error::InvalidMergeCellReference(reference.to_string()))?;
288        self.merge_cells.push(reference.to_string());
289        Ok(())
290    }
291
292    /// Consume the writer and return a [`StreamedSheetData`] containing the
293    /// temp file and pre-built XML fragments for the worksheet header/footer.
294    pub fn into_streamed_data(mut self) -> Result<(String, StreamedSheetData)> {
295        if self.finished {
296            return Err(Error::StreamAlreadyFinished);
297        }
298        self.finished = true;
299
300        // Build XML fragments before consuming the writer.
301        let sheet_views_xml = self.build_sheet_views_xml();
302        let cols_xml = self.build_cols_xml();
303        let merge_cells_xml = self.build_merge_cells_xml();
304        let bytes_written = self.bytes_written;
305        let sheet_name = self.sheet_name.clone();
306
307        // Flush the writer and recover the temp file.
308        self.writer.flush()?;
309        let temp_file = self
310            .writer
311            .into_inner()
312            .map_err(|e| Error::Io(e.into_error()))?;
313
314        let data = StreamedSheetData {
315            temp_file,
316            data_len: bytes_written,
317            sheet_views_xml,
318            cols_xml,
319            merge_cells_xml,
320        };
321        Ok((sheet_name, data))
322    }
323
324    /// Validate that column configuration is allowed (not finished, not started,
325    /// valid column number).
326    fn ensure_col_configurable(&self, col: u32) -> Result<()> {
327        if self.finished {
328            return Err(Error::StreamAlreadyFinished);
329        }
330        if self.started {
331            return Err(Error::StreamColumnsAfterRows);
332        }
333        if col == 0 || col > MAX_COLUMNS {
334            return Err(Error::InvalidColumnNumber(col));
335        }
336        Ok(())
337    }
338
339    /// Get or create a StreamColOptions entry for the given column.
340    fn get_or_create_col_options(&mut self, col: u32) -> &mut StreamColOptions {
341        if let Some(pos) = self.col_options.iter().position(|(c, _)| *c == col) {
342            &mut self.col_options[pos].1
343        } else {
344            self.col_options.push((col, StreamColOptions::default()));
345            let last = self.col_options.len() - 1;
346            &mut self.col_options[last].1
347        }
348    }
349
350    /// Build the `<sheetViews>` XML fragment for freeze panes.
351    fn build_sheet_views_xml(&self) -> Option<String> {
352        let (x_split, y_split, top_left_cell) = self.freeze_pane.as_ref()?;
353        let active_pane = match (*x_split > 0, *y_split > 0) {
354            (true, true) => "bottomRight",
355            (true, false) => "topRight",
356            (false, true) => "bottomLeft",
357            (false, false) => unreachable!(),
358        };
359
360        let sheet_views = SheetViews {
361            sheet_views: vec![SheetView {
362                tab_selected: Some(true),
363                show_grid_lines: None,
364                show_formulas: None,
365                show_row_col_headers: None,
366                zoom_scale: None,
367                view: None,
368                top_left_cell: None,
369                workbook_view_id: 0,
370                pane: Some(Pane {
371                    x_split: if *x_split > 0 { Some(*x_split) } else { None },
372                    y_split: if *y_split > 0 { Some(*y_split) } else { None },
373                    top_left_cell: Some(top_left_cell.clone()),
374                    active_pane: Some(active_pane.to_string()),
375                    state: Some("frozen".to_string()),
376                }),
377                selection: vec![],
378            }],
379        };
380
381        quick_xml::se::to_string_with_root("sheetViews", &sheet_views).ok()
382    }
383
384    /// Build the `<cols>` XML fragment.
385    fn build_cols_xml(&self) -> Option<String> {
386        let has_widths = !self.col_widths.is_empty();
387        let has_options = !self.col_options.is_empty();
388        if !has_widths && !has_options {
389            return None;
390        }
391
392        let mut col_defs = Vec::new();
393        for &(min, max, width) in &self.col_widths {
394            col_defs.push(Col {
395                min,
396                max,
397                width: Some(width),
398                style: None,
399                hidden: None,
400                custom_width: Some(true),
401                outline_level: None,
402            });
403        }
404        for &(col_num, ref opts) in &self.col_options {
405            col_defs.push(Col {
406                min: col_num,
407                max: col_num,
408                width: opts.width,
409                style: opts.style_id,
410                hidden: opts.hidden,
411                custom_width: if opts.width.is_some() {
412                    Some(true)
413                } else {
414                    None
415                },
416                outline_level: opts.outline_level.filter(|&l| l > 0),
417            });
418        }
419
420        let cols = Cols { cols: col_defs };
421        quick_xml::se::to_string_with_root("cols", &cols).ok()
422    }
423
424    /// Build the `<mergeCells>` XML fragment.
425    fn build_merge_cells_xml(&self) -> Option<String> {
426        if self.merge_cells.is_empty() {
427            return None;
428        }
429
430        let mc = MergeCells {
431            count: Some(self.merge_cells.len() as u32),
432            merge_cells: self
433                .merge_cells
434                .iter()
435                .map(|r| MergeCell {
436                    reference: r.clone(),
437                })
438                .collect(),
439            cached_coords: Vec::new(),
440        };
441        quick_xml::se::to_string_with_root("mergeCells", &mc).ok()
442    }
443
444    /// Internal unified implementation for writing a row.
445    /// Serializes the row to XML and writes it directly to the temp file.
446    fn write_row_impl(
447        &mut self,
448        row: u32,
449        values: &[CellValue],
450        cell_style_id: Option<u32>,
451        options: Option<&StreamRowOptions>,
452    ) -> Result<()> {
453        if self.finished {
454            return Err(Error::StreamAlreadyFinished);
455        }
456
457        // Validate row number.
458        if row == 0 || row > MAX_ROWS {
459            return Err(Error::InvalidRowNumber(row));
460        }
461
462        // Enforce ascending row order.
463        if row <= self.last_row {
464            return Err(Error::StreamRowAlreadyWritten { row });
465        }
466
467        // Validate column count.
468        if values.len() > MAX_COLUMNS as usize {
469            return Err(Error::InvalidColumnNumber(values.len() as u32));
470        }
471
472        // Validate row options if provided.
473        if let Some(opts) = options {
474            if let Some(height) = opts.height {
475                if height > MAX_ROW_HEIGHT {
476                    return Err(Error::RowHeightExceeded {
477                        height,
478                        max: MAX_ROW_HEIGHT,
479                    });
480                }
481            }
482            if let Some(level) = opts.outline_level {
483                if level > MAX_OUTLINE_LEVEL {
484                    return Err(Error::OutlineLevelExceeded {
485                        level,
486                        max: MAX_OUTLINE_LEVEL,
487                    });
488                }
489            }
490        }
491
492        self.started = true;
493        self.last_row = row;
494
495        // Build row XML directly and write to temp file.
496        let xml = build_row_xml(row, values, cell_style_id, options);
497        let bytes = xml.as_bytes();
498        self.writer.write_all(bytes)?;
499        self.bytes_written += bytes.len() as u64;
500
501        Ok(())
502    }
503}
504
505/// Build the XML for the worksheet opening, including the XML declaration,
506/// worksheet namespace, sheetViews, cols, and the opening `<sheetData>` tag.
507pub(crate) fn build_worksheet_header(streamed: &StreamedSheetData) -> String {
508    let mut xml = String::with_capacity(512);
509    xml.push_str(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>"#);
510    xml.push('\n');
511    xml.push_str(
512        r#"<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">"#,
513    );
514
515    if let Some(ref views) = streamed.sheet_views_xml {
516        xml.push_str(views);
517    }
518    if let Some(ref cols) = streamed.cols_xml {
519        xml.push_str(cols);
520    }
521
522    xml.push_str("<sheetData>");
523    xml
524}
525
526/// Build the XML for the worksheet closing: `</sheetData>`, mergeCells, and
527/// `</worksheet>`.
528pub(crate) fn build_worksheet_footer(streamed: &StreamedSheetData) -> String {
529    let mut xml = String::with_capacity(256);
530    xml.push_str("</sheetData>");
531
532    if let Some(ref mc) = streamed.merge_cells_xml {
533        xml.push_str(mc);
534    }
535
536    xml.push_str("</worksheet>");
537    xml
538}
539
540/// Write streamed sheet data to a ZIP writer. Composes the full worksheet
541/// XML by writing the header, streaming rows from the temp file, and
542/// appending the footer.
543pub(crate) fn write_streamed_sheet<W: std::io::Write + std::io::Seek>(
544    zip: &mut zip::ZipWriter<W>,
545    entry_name: &str,
546    streamed: &StreamedSheetData,
547    options: zip::write::SimpleFileOptions,
548) -> Result<()> {
549    zip.start_file(entry_name, options)
550        .map_err(|e| Error::Zip(e.to_string()))?;
551
552    // Write the header (XML declaration, <worksheet>, <sheetViews>, <cols>, <sheetData>).
553    let header = build_worksheet_header(streamed);
554    zip.write_all(header.as_bytes())?;
555
556    // Open the temp file by path for reading (starts at position 0).
557    let file = std::fs::File::open(streamed.temp_file.path())?;
558    let mut reader = std::io::BufReader::new(file);
559    std::io::copy(&mut reader, zip)?;
560
561    // Write the footer (</sheetData>, <mergeCells>, </worksheet>).
562    let footer = build_worksheet_footer(streamed);
563    zip.write_all(footer.as_bytes())?;
564
565    Ok(())
566}
567
568/// Build an XML `<row>` element with inline strings for a single row.
569fn build_row_xml(
570    row: u32,
571    values: &[CellValue],
572    cell_style_id: Option<u32>,
573    options: Option<&StreamRowOptions>,
574) -> String {
575    let mut xml = String::with_capacity(128 + values.len() * 64);
576    xml.push_str("<row r=\"");
577    xml.push_str(&row.to_string());
578    xml.push('"');
579
580    // Row-level attributes from options.
581    if let Some(opts) = options {
582        if let Some(sid) = opts.style_id {
583            xml.push_str(" s=\"");
584            xml.push_str(&sid.to_string());
585            xml.push_str("\" customFormat=\"1\"");
586        }
587        if let Some(height) = opts.height {
588            xml.push_str(" ht=\"");
589            xml.push_str(&height.to_string());
590            xml.push_str("\" customHeight=\"1\"");
591        }
592        if let Some(false) = opts.visible {
593            xml.push_str(" hidden=\"1\"");
594        }
595        if let Some(level) = opts.outline_level {
596            if level > 0 {
597                xml.push_str(" outlineLevel=\"");
598                xml.push_str(&level.to_string());
599                xml.push('"');
600            }
601        }
602    }
603
604    xml.push('>');
605
606    for (i, value) in values.iter().enumerate() {
607        let col = (i as u32) + 1;
608        if matches!(value, CellValue::Empty) {
609            continue;
610        }
611
612        let cell_ref = crate::utils::cell_ref::coordinates_to_cell_name(col, row)
613            .unwrap_or_else(|_| format!("{col}:{row}"));
614
615        xml.push_str("<c r=\"");
616        xml.push_str(&cell_ref);
617        xml.push('"');
618
619        // Style attribute.
620        if let Some(sid) = cell_style_id {
621            xml.push_str(" s=\"");
622            xml.push_str(&sid.to_string());
623            xml.push('"');
624        }
625
626        match value {
627            CellValue::String(s) => {
628                xml.push_str(" t=\"inlineStr\"><is><t>");
629                xml_escape_into(&mut xml, s);
630                xml.push_str("</t></is></c>");
631            }
632            CellValue::Number(n) => {
633                xml.push_str("><v>");
634                xml.push_str(&n.to_string());
635                xml.push_str("</v></c>");
636            }
637            CellValue::Date(serial) => {
638                xml.push_str("><v>");
639                xml.push_str(&serial.to_string());
640                xml.push_str("</v></c>");
641            }
642            CellValue::Bool(b) => {
643                xml.push_str(" t=\"b\"><v>");
644                xml.push_str(if *b { "1" } else { "0" });
645                xml.push_str("</v></c>");
646            }
647            CellValue::Formula { expr, result } => {
648                // Set the cell type attribute based on the cached result type.
649                // Without the correct t attribute, xml_cell_to_value cannot
650                // decode the result on reopen.
651                if let Some(res) = result {
652                    match res.as_ref() {
653                        CellValue::String(_) => xml.push_str(" t=\"str\""),
654                        CellValue::Bool(_) => xml.push_str(" t=\"b\""),
655                        CellValue::Error(_) => xml.push_str(" t=\"e\""),
656                        _ => {} // Number/Date: default (no t) is numeric
657                    }
658                }
659                xml.push_str("><f>");
660                xml_escape_into(&mut xml, expr);
661                xml.push_str("</f>");
662                if let Some(res) = result {
663                    match res.as_ref() {
664                        CellValue::String(s) => {
665                            xml.push_str("<v>");
666                            xml_escape_into(&mut xml, s);
667                            xml.push_str("</v>");
668                        }
669                        CellValue::Number(n) => {
670                            xml.push_str("<v>");
671                            xml.push_str(&n.to_string());
672                            xml.push_str("</v>");
673                        }
674                        CellValue::Bool(b) => {
675                            xml.push_str("<v>");
676                            xml.push_str(if *b { "1" } else { "0" });
677                            xml.push_str("</v>");
678                        }
679                        CellValue::Date(d) => {
680                            xml.push_str("<v>");
681                            xml.push_str(&d.to_string());
682                            xml.push_str("</v>");
683                        }
684                        CellValue::Error(e) => {
685                            xml.push_str("<v>");
686                            xml_escape_into(&mut xml, e);
687                            xml.push_str("</v>");
688                        }
689                        _ => {}
690                    }
691                }
692                xml.push_str("</c>");
693            }
694            CellValue::Error(e) => {
695                xml.push_str(" t=\"e\"><v>");
696                xml_escape_into(&mut xml, e);
697                xml.push_str("</v></c>");
698            }
699            CellValue::RichString(runs) => {
700                let plain = crate::rich_text::rich_text_to_plain(runs);
701                xml.push_str(" t=\"inlineStr\"><is><t>");
702                xml_escape_into(&mut xml, &plain);
703                xml.push_str("</t></is></c>");
704            }
705            CellValue::Empty => unreachable!(),
706        }
707    }
708
709    xml.push_str("</row>");
710    xml
711}
712
713/// Escape XML special characters into an existing string buffer.
714fn xml_escape_into(buf: &mut String, s: &str) {
715    for ch in s.chars() {
716        match ch {
717            '&' => buf.push_str("&amp;"),
718            '<' => buf.push_str("&lt;"),
719            '>' => buf.push_str("&gt;"),
720            '"' => buf.push_str("&quot;"),
721            '\'' => buf.push_str("&apos;"),
722            _ => buf.push(ch),
723        }
724    }
725}
726
727#[cfg(test)]
728mod tests {
729    use super::*;
730    use sheetkit_xml::worksheet::WorksheetXml;
731    use std::io::{Read as _, Seek as _};
732
733    /// Helper to parse a streamed writer into a WorksheetXml for assertion.
734    fn finish_and_parse(sw: StreamWriter) -> WorksheetXml {
735        let (_, mut streamed) = sw.into_streamed_data().unwrap();
736        let header = build_worksheet_header(&streamed);
737        let footer = build_worksheet_footer(&streamed);
738
739        let file = streamed.temp_file.as_file_mut();
740        file.seek(std::io::SeekFrom::Start(0)).unwrap();
741        let mut row_data = String::new();
742        file.read_to_string(&mut row_data).unwrap();
743
744        let full_xml = format!("{header}{row_data}{footer}");
745        quick_xml::de::from_str(&full_xml).unwrap()
746    }
747
748    /// Helper to get the raw XML from a stream writer.
749    fn finish_and_get_xml(sw: StreamWriter) -> String {
750        let (_, mut streamed) = sw.into_streamed_data().unwrap();
751        let header = build_worksheet_header(&streamed);
752        let footer = build_worksheet_footer(&streamed);
753
754        let file = streamed.temp_file.as_file_mut();
755        file.seek(std::io::SeekFrom::Start(0)).unwrap();
756        let mut row_data = String::new();
757        file.read_to_string(&mut row_data).unwrap();
758
759        format!("{header}{row_data}{footer}")
760    }
761
762    #[test]
763    fn test_basic_write_and_finish() {
764        let mut sw = StreamWriter::new("Sheet1");
765        sw.write_row(1, &[CellValue::from("Name"), CellValue::from("Age")])
766            .unwrap();
767        sw.write_row(2, &[CellValue::from("Alice"), CellValue::from(30)])
768            .unwrap();
769        let xml = finish_and_get_xml(sw);
770
771        assert!(xml.contains("<?xml version=\"1.0\""));
772        assert!(xml.contains("<worksheet"));
773        assert!(xml.contains("<sheetData>"));
774        assert!(xml.contains("</sheetData>"));
775        assert!(xml.contains("</worksheet>"));
776    }
777
778    #[test]
779    fn test_parse_output_xml_back() {
780        let mut sw = StreamWriter::new("TestSheet");
781        sw.write_row(1, &[CellValue::from("Hello"), CellValue::from(42)])
782            .unwrap();
783        let ws = finish_and_parse(sw);
784
785        assert_eq!(ws.sheet_data.rows.len(), 1);
786        assert_eq!(ws.sheet_data.rows[0].r, 1);
787        assert_eq!(ws.sheet_data.rows[0].cells.len(), 2);
788        // First cell is an inline string (t="inlineStr")
789        assert_eq!(
790            ws.sheet_data.rows[0].cells[0].t,
791            sheetkit_xml::worksheet::CellTypeTag::InlineString
792        );
793        assert_eq!(ws.sheet_data.rows[0].cells[0].r, "A1");
794        // Second cell is a number
795        assert_eq!(
796            ws.sheet_data.rows[0].cells[1].t,
797            sheetkit_xml::worksheet::CellTypeTag::None
798        );
799        assert_eq!(ws.sheet_data.rows[0].cells[1].v, Some("42".to_string()));
800        assert_eq!(ws.sheet_data.rows[0].cells[1].r, "B1");
801    }
802
803    #[test]
804    fn test_inline_strings_used() {
805        let mut sw = StreamWriter::new("Sheet1");
806        sw.write_row(1, &[CellValue::from("Hello"), CellValue::from("World")])
807            .unwrap();
808        let xml = finish_and_get_xml(sw);
809
810        // Should use inline strings, not shared string references
811        assert!(xml.contains("t=\"inlineStr\""));
812        assert!(xml.contains("<is><t>Hello</t></is>"));
813        assert!(xml.contains("<is><t>World</t></is>"));
814        // Should NOT contain shared string references
815        assert!(!xml.contains("t=\"s\""));
816    }
817
818    #[test]
819    fn test_number_value() {
820        let mut sw = StreamWriter::new("Sheet1");
821        sw.write_row(1, &[CellValue::from(3.15)]).unwrap();
822        let xml = finish_and_get_xml(sw);
823
824        assert!(xml.contains("<v>3.15</v>"));
825    }
826
827    #[test]
828    fn test_bool_values() {
829        let mut sw = StreamWriter::new("Sheet1");
830        sw.write_row(1, &[CellValue::from(true), CellValue::from(false)])
831            .unwrap();
832        let xml = finish_and_get_xml(sw);
833
834        assert!(xml.contains("<v>1</v>"));
835        assert!(xml.contains("<v>0</v>"));
836    }
837
838    #[test]
839    fn test_formula_value() {
840        let mut sw = StreamWriter::new("Sheet1");
841        sw.write_row(
842            1,
843            &[CellValue::Formula {
844                expr: "SUM(A2:A10)".to_string(),
845                result: None,
846            }],
847        )
848        .unwrap();
849        let xml = finish_and_get_xml(sw);
850
851        assert!(xml.contains("SUM(A2:A10)"));
852    }
853
854    #[test]
855    fn test_error_value() {
856        let mut sw = StreamWriter::new("Sheet1");
857        sw.write_row(1, &[CellValue::Error("#DIV/0!".to_string())])
858            .unwrap();
859        let xml = finish_and_get_xml(sw);
860
861        assert!(xml.contains("#DIV/0!"));
862    }
863
864    #[test]
865    fn test_empty_values_are_skipped() {
866        let mut sw = StreamWriter::new("Sheet1");
867        sw.write_row(
868            1,
869            &[CellValue::from("A"), CellValue::Empty, CellValue::from("C")],
870        )
871        .unwrap();
872        let ws = finish_and_parse(sw);
873
874        // 2 cells (Empty is skipped)
875        assert_eq!(ws.sheet_data.rows[0].cells.len(), 2);
876        assert_eq!(ws.sheet_data.rows[0].cells[0].r, "A1");
877        assert_eq!(ws.sheet_data.rows[0].cells[1].r, "C1");
878    }
879
880    #[test]
881    fn test_write_row_with_style() {
882        let mut sw = StreamWriter::new("Sheet1");
883        sw.write_row_with_style(1, &[CellValue::from("Styled")], 5)
884            .unwrap();
885        let xml = finish_and_get_xml(sw);
886
887        assert!(xml.contains("s=\"5\""));
888    }
889
890    #[test]
891    fn test_set_col_width_before_rows() {
892        let mut sw = StreamWriter::new("Sheet1");
893        sw.set_col_width(1, 20.0).unwrap();
894        sw.write_row(1, &[CellValue::from("data")]).unwrap();
895        let ws = finish_and_parse(sw);
896
897        let cols = ws.cols.unwrap();
898        assert_eq!(cols.cols.len(), 1);
899        assert_eq!(cols.cols[0].min, 1);
900        assert_eq!(cols.cols[0].max, 1);
901        assert_eq!(cols.cols[0].width, Some(20.0));
902        assert_eq!(cols.cols[0].custom_width, Some(true));
903    }
904
905    #[test]
906    fn test_set_col_width_range() {
907        let mut sw = StreamWriter::new("Sheet1");
908        sw.set_col_width_range(1, 3, 15.5).unwrap();
909        let ws = finish_and_parse(sw);
910
911        let cols = ws.cols.unwrap();
912        assert_eq!(cols.cols.len(), 1);
913        assert_eq!(cols.cols[0].min, 1);
914        assert_eq!(cols.cols[0].max, 3);
915        assert_eq!(cols.cols[0].width, Some(15.5));
916    }
917
918    #[test]
919    fn test_col_width_in_output_xml() {
920        let mut sw = StreamWriter::new("Sheet1");
921        sw.set_col_width(2, 25.0).unwrap();
922        sw.write_row(1, &[CellValue::from("data")]).unwrap();
923        let xml = finish_and_get_xml(sw);
924
925        // Verify cols section appears before sheetData
926        let cols_pos = xml.find("<cols>").unwrap();
927        let sheet_data_pos = xml.find("<sheetData").unwrap();
928        assert!(cols_pos < sheet_data_pos);
929    }
930
931    #[test]
932    fn test_col_width_after_rows_returns_error() {
933        let mut sw = StreamWriter::new("Sheet1");
934        sw.write_row(1, &[CellValue::from("data")]).unwrap();
935        let result = sw.set_col_width(1, 20.0);
936        assert!(result.is_err());
937        assert!(matches!(result.unwrap_err(), Error::StreamColumnsAfterRows));
938    }
939
940    #[test]
941    fn test_rows_in_order_succeeds() {
942        let mut sw = StreamWriter::new("Sheet1");
943        sw.write_row(1, &[CellValue::from("a")]).unwrap();
944        sw.write_row(2, &[CellValue::from("b")]).unwrap();
945        sw.write_row(3, &[CellValue::from("c")]).unwrap();
946        let _data = sw.into_streamed_data().unwrap();
947    }
948
949    #[test]
950    fn test_rows_with_gaps_succeeds() {
951        let mut sw = StreamWriter::new("Sheet1");
952        sw.write_row(1, &[CellValue::from("a")]).unwrap();
953        sw.write_row(3, &[CellValue::from("b")]).unwrap();
954        sw.write_row(5, &[CellValue::from("c")]).unwrap();
955        let ws = finish_and_parse(sw);
956
957        assert_eq!(ws.sheet_data.rows.len(), 3);
958        assert_eq!(ws.sheet_data.rows[0].r, 1);
959        assert_eq!(ws.sheet_data.rows[1].r, 3);
960        assert_eq!(ws.sheet_data.rows[2].r, 5);
961    }
962
963    #[test]
964    fn test_duplicate_row_number_fails() {
965        let mut sw = StreamWriter::new("Sheet1");
966        sw.write_row(1, &[CellValue::from("a")]).unwrap();
967        let result = sw.write_row(1, &[CellValue::from("b")]);
968        assert!(result.is_err());
969        assert!(matches!(
970            result.unwrap_err(),
971            Error::StreamRowAlreadyWritten { row: 1 }
972        ));
973    }
974
975    #[test]
976    fn test_row_zero_fails() {
977        let mut sw = StreamWriter::new("Sheet1");
978        let result = sw.write_row(0, &[CellValue::from("a")]);
979        assert!(result.is_err());
980        assert!(matches!(result.unwrap_err(), Error::InvalidRowNumber(0)));
981    }
982
983    #[test]
984    fn test_row_out_of_order_fails() {
985        let mut sw = StreamWriter::new("Sheet1");
986        sw.write_row(5, &[CellValue::from("a")]).unwrap();
987        let result = sw.write_row(3, &[CellValue::from("b")]);
988        assert!(result.is_err());
989        assert!(matches!(
990            result.unwrap_err(),
991            Error::StreamRowAlreadyWritten { row: 3 }
992        ));
993    }
994
995    #[test]
996    fn test_merge_cells_in_output() {
997        let mut sw = StreamWriter::new("Sheet1");
998        sw.write_row(1, &[CellValue::from("Merged")]).unwrap();
999        sw.add_merge_cell("A1:B1").unwrap();
1000        let ws = finish_and_parse(sw);
1001
1002        let mc = ws.merge_cells.unwrap();
1003        assert_eq!(mc.merge_cells.len(), 1);
1004        assert_eq!(mc.merge_cells[0].reference, "A1:B1");
1005    }
1006
1007    #[test]
1008    fn test_multiple_merge_cells() {
1009        let mut sw = StreamWriter::new("Sheet1");
1010        sw.write_row(1, &[CellValue::from("a")]).unwrap();
1011        sw.add_merge_cell("A1:B1").unwrap();
1012        sw.add_merge_cell("C1:D1").unwrap();
1013        let ws = finish_and_parse(sw);
1014
1015        let mc = ws.merge_cells.unwrap();
1016        assert_eq!(mc.merge_cells.len(), 2);
1017        assert_eq!(mc.merge_cells[0].reference, "A1:B1");
1018        assert_eq!(mc.merge_cells[1].reference, "C1:D1");
1019    }
1020
1021    #[test]
1022    fn test_finish_twice_returns_error() {
1023        let mut sw = StreamWriter::new("Sheet1");
1024        sw.write_row(1, &[CellValue::from("a")]).unwrap();
1025        // First consume
1026        sw.into_streamed_data().unwrap();
1027        // Cannot call again -- consumed by move.
1028    }
1029
1030    #[test]
1031    fn test_write_after_finish_returns_error() {
1032        // StreamWriter is consumed by into_streamed_data(), so this test
1033        // verifies that the finished flag works for the legacy path.
1034        let mut sw = StreamWriter::new("Sheet1");
1035        sw.write_row(1, &[CellValue::from("a")]).unwrap();
1036        sw.finished = true;
1037        let result = sw.write_row(2, &[CellValue::from("b")]);
1038        assert!(result.is_err());
1039        assert!(matches!(result.unwrap_err(), Error::StreamAlreadyFinished));
1040    }
1041
1042    #[test]
1043    fn test_finish_with_no_rows() {
1044        let sw = StreamWriter::new("Sheet1");
1045        let ws = finish_and_parse(sw);
1046
1047        // Should still produce valid XML
1048        assert!(ws.sheet_data.rows.is_empty());
1049    }
1050
1051    #[test]
1052    fn test_finish_with_cols_and_no_rows() {
1053        let mut sw = StreamWriter::new("Sheet1");
1054        sw.set_col_width(1, 20.0).unwrap();
1055        let ws = finish_and_parse(sw);
1056
1057        assert!(ws.cols.is_some());
1058        assert!(ws.sheet_data.rows.is_empty());
1059    }
1060
1061    #[test]
1062    fn test_add_merge_cell_invalid_reference() {
1063        let mut sw = StreamWriter::new("Sheet1");
1064        // Missing colon separator.
1065        let result = sw.add_merge_cell("A1B2");
1066        assert!(result.is_err());
1067        assert!(matches!(
1068            result.unwrap_err(),
1069            Error::InvalidMergeCellReference(_)
1070        ));
1071    }
1072
1073    #[test]
1074    fn test_add_merge_cell_invalid_cell_name() {
1075        let mut sw = StreamWriter::new("Sheet1");
1076        // Invalid cell name before colon.
1077        let result = sw.add_merge_cell("ZZZ:B2");
1078        assert!(result.is_err());
1079        assert!(matches!(
1080            result.unwrap_err(),
1081            Error::InvalidMergeCellReference(_)
1082        ));
1083    }
1084
1085    #[test]
1086    fn test_add_merge_cell_empty_reference() {
1087        let mut sw = StreamWriter::new("Sheet1");
1088        let result = sw.add_merge_cell("");
1089        assert!(result.is_err());
1090        assert!(matches!(
1091            result.unwrap_err(),
1092            Error::InvalidMergeCellReference(_)
1093        ));
1094    }
1095
1096    #[test]
1097    fn test_add_merge_cell_after_finish_fails() {
1098        let mut sw = StreamWriter::new("Sheet1");
1099        sw.finished = true;
1100        let result = sw.add_merge_cell("A1:B1");
1101        assert!(result.is_err());
1102        assert!(matches!(result.unwrap_err(), Error::StreamAlreadyFinished));
1103    }
1104
1105    #[test]
1106    fn test_set_col_width_after_finish_fails() {
1107        let mut sw = StreamWriter::new("Sheet1");
1108        sw.finished = true;
1109        let result = sw.set_col_width(1, 10.0);
1110        assert!(result.is_err());
1111        assert!(matches!(result.unwrap_err(), Error::StreamAlreadyFinished));
1112    }
1113
1114    #[test]
1115    fn test_sheet_name_getter() {
1116        let sw = StreamWriter::new("MySheet");
1117        assert_eq!(sw.sheet_name(), "MySheet");
1118    }
1119
1120    #[test]
1121    fn test_all_value_types_in_single_row() {
1122        let mut sw = StreamWriter::new("Sheet1");
1123        sw.write_row(
1124            1,
1125            &[
1126                CellValue::from("text"),
1127                CellValue::from(42),
1128                CellValue::from(3.15),
1129                CellValue::from(true),
1130                CellValue::from(false),
1131                CellValue::Formula {
1132                    expr: "A1+B1".to_string(),
1133                    result: None,
1134                },
1135                CellValue::Error("#N/A".to_string()),
1136                CellValue::Empty,
1137            ],
1138        )
1139        .unwrap();
1140        let ws = finish_and_parse(sw);
1141
1142        // 7 cells (Empty is skipped)
1143        assert_eq!(ws.sheet_data.rows[0].cells.len(), 7);
1144    }
1145
1146    #[test]
1147    fn test_col_width_invalid_column_zero() {
1148        let mut sw = StreamWriter::new("Sheet1");
1149        let result = sw.set_col_width(0, 10.0);
1150        assert!(result.is_err());
1151        assert!(matches!(result.unwrap_err(), Error::InvalidColumnNumber(0)));
1152    }
1153
1154    #[test]
1155    fn test_write_row_with_options_height() {
1156        let mut sw = StreamWriter::new("Sheet1");
1157        let opts = StreamRowOptions {
1158            height: Some(30.0),
1159            ..Default::default()
1160        };
1161        sw.write_row_with_options(1, &[CellValue::from("tall row")], &opts)
1162            .unwrap();
1163        let ws = finish_and_parse(sw);
1164
1165        assert_eq!(ws.sheet_data.rows[0].ht, Some(30.0));
1166        assert_eq!(ws.sheet_data.rows[0].custom_height, Some(true));
1167    }
1168
1169    #[test]
1170    fn test_write_row_with_options_hidden() {
1171        let mut sw = StreamWriter::new("Sheet1");
1172        let opts = StreamRowOptions {
1173            visible: Some(false),
1174            ..Default::default()
1175        };
1176        sw.write_row_with_options(1, &[CellValue::from("hidden")], &opts)
1177            .unwrap();
1178        let ws = finish_and_parse(sw);
1179
1180        assert_eq!(ws.sheet_data.rows[0].hidden, Some(true));
1181    }
1182
1183    #[test]
1184    fn test_write_row_with_options_outline_level() {
1185        let mut sw = StreamWriter::new("Sheet1");
1186        let opts = StreamRowOptions {
1187            outline_level: Some(3),
1188            ..Default::default()
1189        };
1190        sw.write_row_with_options(1, &[CellValue::from("grouped")], &opts)
1191            .unwrap();
1192        let ws = finish_and_parse(sw);
1193
1194        assert_eq!(ws.sheet_data.rows[0].outline_level, Some(3));
1195    }
1196
1197    #[test]
1198    fn test_write_row_with_options_style() {
1199        let mut sw = StreamWriter::new("Sheet1");
1200        let opts = StreamRowOptions {
1201            style_id: Some(2),
1202            ..Default::default()
1203        };
1204        sw.write_row_with_options(1, &[CellValue::from("styled row")], &opts)
1205            .unwrap();
1206        let ws = finish_and_parse(sw);
1207
1208        assert_eq!(ws.sheet_data.rows[0].s, Some(2));
1209        assert_eq!(ws.sheet_data.rows[0].custom_format, Some(true));
1210    }
1211
1212    #[test]
1213    fn test_write_row_with_options_all() {
1214        let mut sw = StreamWriter::new("Sheet1");
1215        let opts = StreamRowOptions {
1216            height: Some(25.5),
1217            visible: Some(false),
1218            outline_level: Some(2),
1219            style_id: Some(7),
1220        };
1221        sw.write_row_with_options(1, &[CellValue::from("all options")], &opts)
1222            .unwrap();
1223        let ws = finish_and_parse(sw);
1224
1225        let row = &ws.sheet_data.rows[0];
1226        assert_eq!(row.ht, Some(25.5));
1227        assert_eq!(row.custom_height, Some(true));
1228        assert_eq!(row.hidden, Some(true));
1229        assert_eq!(row.outline_level, Some(2));
1230        assert_eq!(row.s, Some(7));
1231        assert_eq!(row.custom_format, Some(true));
1232    }
1233
1234    #[test]
1235    fn test_write_row_with_options_height_exceeded() {
1236        let mut sw = StreamWriter::new("Sheet1");
1237        let opts = StreamRowOptions {
1238            height: Some(500.0),
1239            ..Default::default()
1240        };
1241        let result = sw.write_row_with_options(1, &[CellValue::from("too tall")], &opts);
1242        assert!(result.is_err());
1243        assert!(matches!(
1244            result.unwrap_err(),
1245            Error::RowHeightExceeded { .. }
1246        ));
1247    }
1248
1249    #[test]
1250    fn test_write_row_with_options_outline_level_exceeded() {
1251        let mut sw = StreamWriter::new("Sheet1");
1252        let opts = StreamRowOptions {
1253            outline_level: Some(8),
1254            ..Default::default()
1255        };
1256        let result = sw.write_row_with_options(1, &[CellValue::from("bad level")], &opts);
1257        assert!(result.is_err());
1258    }
1259
1260    #[test]
1261    fn test_write_row_with_options_visible_true_no_hidden_attr() {
1262        let mut sw = StreamWriter::new("Sheet1");
1263        let opts = StreamRowOptions {
1264            visible: Some(true),
1265            ..Default::default()
1266        };
1267        sw.write_row_with_options(1, &[CellValue::from("visible")], &opts)
1268            .unwrap();
1269        let xml = finish_and_get_xml(sw);
1270
1271        // visible=true should NOT produce hidden attribute
1272        assert!(!xml.contains("hidden="));
1273    }
1274
1275    #[test]
1276    fn test_col_style() {
1277        let mut sw = StreamWriter::new("Sheet1");
1278        sw.set_col_style(1, 3).unwrap();
1279        sw.write_row(1, &[CellValue::from("data")]).unwrap();
1280        let ws = finish_and_parse(sw);
1281
1282        let cols = ws.cols.unwrap();
1283        let styled_col = cols.cols.iter().find(|c| c.style.is_some()).unwrap();
1284        assert_eq!(styled_col.style, Some(3));
1285        assert_eq!(styled_col.min, 1);
1286        assert_eq!(styled_col.max, 1);
1287    }
1288
1289    #[test]
1290    fn test_col_visible() {
1291        let mut sw = StreamWriter::new("Sheet1");
1292        sw.set_col_visible(2, false).unwrap();
1293        sw.write_row(1, &[CellValue::from("a"), CellValue::from("b")])
1294            .unwrap();
1295        let ws = finish_and_parse(sw);
1296
1297        let cols = ws.cols.unwrap();
1298        let hidden_col = cols.cols.iter().find(|c| c.hidden == Some(true)).unwrap();
1299        assert_eq!(hidden_col.min, 2);
1300        assert_eq!(hidden_col.max, 2);
1301    }
1302
1303    #[test]
1304    fn test_col_outline_level() {
1305        let mut sw = StreamWriter::new("Sheet1");
1306        sw.set_col_outline_level(3, 2).unwrap();
1307        sw.write_row(1, &[CellValue::from("a")]).unwrap();
1308        let ws = finish_and_parse(sw);
1309
1310        let cols = ws.cols.unwrap();
1311        let outlined_col = cols
1312            .cols
1313            .iter()
1314            .find(|c| c.outline_level.is_some())
1315            .unwrap();
1316        assert_eq!(outlined_col.outline_level, Some(2));
1317        assert_eq!(outlined_col.min, 3);
1318        assert_eq!(outlined_col.max, 3);
1319    }
1320
1321    #[test]
1322    fn test_col_outline_level_exceeded() {
1323        let mut sw = StreamWriter::new("Sheet1");
1324        let result = sw.set_col_outline_level(1, 8);
1325        assert!(result.is_err());
1326    }
1327
1328    #[test]
1329    fn test_col_style_after_rows_error() {
1330        let mut sw = StreamWriter::new("Sheet1");
1331        sw.write_row(1, &[CellValue::from("data")]).unwrap();
1332        let result = sw.set_col_style(1, 1);
1333        assert!(result.is_err());
1334        assert!(matches!(result.unwrap_err(), Error::StreamColumnsAfterRows));
1335    }
1336
1337    #[test]
1338    fn test_col_visible_after_rows_error() {
1339        let mut sw = StreamWriter::new("Sheet1");
1340        sw.write_row(1, &[CellValue::from("data")]).unwrap();
1341        let result = sw.set_col_visible(1, false);
1342        assert!(result.is_err());
1343        assert!(matches!(result.unwrap_err(), Error::StreamColumnsAfterRows));
1344    }
1345
1346    #[test]
1347    fn test_col_outline_after_rows_error() {
1348        let mut sw = StreamWriter::new("Sheet1");
1349        sw.write_row(1, &[CellValue::from("data")]).unwrap();
1350        let result = sw.set_col_outline_level(1, 1);
1351        assert!(result.is_err());
1352        assert!(matches!(result.unwrap_err(), Error::StreamColumnsAfterRows));
1353    }
1354
1355    #[test]
1356    fn test_freeze_panes_rows() {
1357        let mut sw = StreamWriter::new("Sheet1");
1358        sw.set_freeze_panes("A2").unwrap();
1359        sw.write_row(1, &[CellValue::from("header")]).unwrap();
1360        sw.write_row(2, &[CellValue::from("data")]).unwrap();
1361        let ws = finish_and_parse(sw);
1362
1363        let views = ws.sheet_views.unwrap();
1364        let pane = views.sheet_views[0].pane.as_ref().unwrap();
1365        assert_eq!(pane.y_split, Some(1));
1366        assert_eq!(pane.top_left_cell, Some("A2".to_string()));
1367        assert_eq!(pane.active_pane, Some("bottomLeft".to_string()));
1368        assert_eq!(pane.state, Some("frozen".to_string()));
1369        // xSplit should not appear when only rows are frozen
1370        assert_eq!(pane.x_split, None);
1371    }
1372
1373    #[test]
1374    fn test_freeze_panes_cols() {
1375        let mut sw = StreamWriter::new("Sheet1");
1376        sw.set_freeze_panes("B1").unwrap();
1377        sw.write_row(1, &[CellValue::from("a"), CellValue::from("b")])
1378            .unwrap();
1379        let ws = finish_and_parse(sw);
1380
1381        let views = ws.sheet_views.unwrap();
1382        let pane = views.sheet_views[0].pane.as_ref().unwrap();
1383        assert_eq!(pane.x_split, Some(1));
1384        assert_eq!(pane.top_left_cell, Some("B1".to_string()));
1385        assert_eq!(pane.active_pane, Some("topRight".to_string()));
1386        assert_eq!(pane.state, Some("frozen".to_string()));
1387        // ySplit should not appear when only cols are frozen
1388        assert_eq!(pane.y_split, None);
1389    }
1390
1391    #[test]
1392    fn test_freeze_panes_both() {
1393        let mut sw = StreamWriter::new("Sheet1");
1394        sw.set_freeze_panes("C3").unwrap();
1395        sw.write_row(1, &[CellValue::from("a")]).unwrap();
1396        let ws = finish_and_parse(sw);
1397
1398        let views = ws.sheet_views.unwrap();
1399        let pane = views.sheet_views[0].pane.as_ref().unwrap();
1400        assert_eq!(pane.x_split, Some(2));
1401        assert_eq!(pane.y_split, Some(2));
1402        assert_eq!(pane.top_left_cell, Some("C3".to_string()));
1403        assert_eq!(pane.active_pane, Some("bottomRight".to_string()));
1404        assert_eq!(pane.state, Some("frozen".to_string()));
1405    }
1406
1407    #[test]
1408    fn test_freeze_panes_after_rows_error() {
1409        let mut sw = StreamWriter::new("Sheet1");
1410        sw.write_row(1, &[CellValue::from("data")]).unwrap();
1411        let result = sw.set_freeze_panes("A2");
1412        assert!(result.is_err());
1413        assert!(matches!(result.unwrap_err(), Error::StreamColumnsAfterRows));
1414    }
1415
1416    #[test]
1417    fn test_freeze_panes_a1_error() {
1418        let mut sw = StreamWriter::new("Sheet1");
1419        let result = sw.set_freeze_panes("A1");
1420        assert!(result.is_err());
1421        assert!(matches!(
1422            result.unwrap_err(),
1423            Error::InvalidCellReference(_)
1424        ));
1425    }
1426
1427    #[test]
1428    fn test_freeze_panes_invalid_cell_error() {
1429        let mut sw = StreamWriter::new("Sheet1");
1430        let result = sw.set_freeze_panes("ZZZZ1");
1431        assert!(result.is_err());
1432    }
1433
1434    #[test]
1435    fn test_freeze_panes_appears_before_cols() {
1436        let mut sw = StreamWriter::new("Sheet1");
1437        sw.set_freeze_panes("A2").unwrap();
1438        sw.set_col_width(1, 20.0).unwrap();
1439        sw.write_row(1, &[CellValue::from("data")]).unwrap();
1440        let xml = finish_and_get_xml(sw);
1441
1442        let views_pos = xml.find("<sheetView").unwrap();
1443        let cols_pos = xml.find("<cols>").unwrap();
1444        let data_pos = xml.find("<sheetData").unwrap();
1445        assert!(views_pos < cols_pos);
1446        assert!(cols_pos < data_pos);
1447    }
1448
1449    #[test]
1450    fn test_freeze_panes_after_finish_error() {
1451        let mut sw = StreamWriter::new("Sheet1");
1452        sw.finished = true;
1453        let result = sw.set_freeze_panes("A2");
1454        assert!(result.is_err());
1455        assert!(matches!(result.unwrap_err(), Error::StreamAlreadyFinished));
1456    }
1457
1458    #[test]
1459    fn test_no_freeze_panes_no_sheet_views() {
1460        let mut sw = StreamWriter::new("Sheet1");
1461        sw.write_row(1, &[CellValue::from("data")]).unwrap();
1462        let xml = finish_and_get_xml(sw);
1463
1464        // When no freeze panes are set, sheetViews should not appear
1465        assert!(!xml.contains("<sheetView"));
1466    }
1467
1468    #[test]
1469    fn test_write_row_backward_compat() {
1470        // Ensure the original write_row still works exactly as before.
1471        let mut sw = StreamWriter::new("Sheet1");
1472        sw.write_row(1, &[CellValue::from("hello")]).unwrap();
1473        let ws = finish_and_parse(sw);
1474
1475        let row = &ws.sheet_data.rows[0];
1476        assert_eq!(row.r, 1);
1477        // Should not have any row-level attributes beyond r
1478        assert!(row.ht.is_none());
1479        assert!(row.hidden.is_none());
1480        assert!(row.outline_level.is_none());
1481        assert!(row.custom_format.is_none());
1482    }
1483
1484    #[test]
1485    fn test_col_options_combined_with_widths() {
1486        let mut sw = StreamWriter::new("Sheet1");
1487        sw.set_col_width(1, 20.0).unwrap();
1488        sw.set_col_style(2, 5).unwrap();
1489        sw.set_col_visible(3, false).unwrap();
1490        sw.write_row(
1491            1,
1492            &[
1493                CellValue::from("a"),
1494                CellValue::from("b"),
1495                CellValue::from("c"),
1496            ],
1497        )
1498        .unwrap();
1499        let ws = finish_and_parse(sw);
1500
1501        let cols = ws.cols.unwrap();
1502        assert_eq!(cols.cols.len(), 3);
1503    }
1504
1505    #[test]
1506    fn test_large_scale_10000_rows() {
1507        let mut sw = StreamWriter::new("BigSheet");
1508        for i in 1..=10_000u32 {
1509            sw.write_row(
1510                i,
1511                &[
1512                    CellValue::from(format!("Row {}", i)),
1513                    CellValue::from(i as i32),
1514                    CellValue::from(i as f64 * 1.5),
1515                ],
1516            )
1517            .unwrap();
1518        }
1519        let ws = finish_and_parse(sw);
1520
1521        assert_eq!(ws.sheet_data.rows.len(), 10_000);
1522        assert_eq!(ws.sheet_data.rows[0].r, 1);
1523        assert_eq!(ws.sheet_data.rows[9999].r, 10_000);
1524    }
1525
1526    #[test]
1527    fn test_xml_escape_in_formula() {
1528        let mut sw = StreamWriter::new("Sheet1");
1529        sw.write_row(
1530            1,
1531            &[CellValue::Formula {
1532                expr: "IF(A1>0,\"yes\",\"no\")".to_string(),
1533                result: None,
1534            }],
1535        )
1536        .unwrap();
1537        let xml = finish_and_get_xml(sw);
1538
1539        // The formula should be XML-escaped
1540        assert!(xml.contains("&gt;"));
1541    }
1542
1543    #[test]
1544    fn test_xml_escape_in_string() {
1545        let mut sw = StreamWriter::new("Sheet1");
1546        sw.write_row(1, &[CellValue::from("Tom & Jerry <friends>")])
1547            .unwrap();
1548        let xml = finish_and_get_xml(sw);
1549
1550        assert!(xml.contains("Tom &amp; Jerry &lt;friends&gt;"));
1551    }
1552
1553    #[test]
1554    fn test_into_streamed_data_returns_valid_data() {
1555        let mut sw = StreamWriter::new("Sheet1");
1556        sw.set_col_width(1, 20.0).unwrap();
1557        sw.write_row(1, &[CellValue::from("Hello"), CellValue::from(42)])
1558            .unwrap();
1559        sw.write_row(2, &[CellValue::from("World"), CellValue::from(99)])
1560            .unwrap();
1561        sw.add_merge_cell("A1:B1").unwrap();
1562
1563        let (name, streamed) = sw.into_streamed_data().unwrap();
1564        assert_eq!(name, "Sheet1");
1565        assert!(streamed.data_len > 0);
1566        assert!(streamed.cols_xml.is_some());
1567        assert!(streamed.merge_cells_xml.is_some());
1568    }
1569
1570    #[test]
1571    fn test_streamed_sheet_data_try_clone() {
1572        let mut sw = StreamWriter::new("Sheet1");
1573        sw.set_col_width(1, 20.0).unwrap();
1574        sw.set_freeze_panes("A2").unwrap();
1575        sw.write_row(1, &[CellValue::from("Hello")]).unwrap();
1576        sw.write_row(2, &[CellValue::from("World")]).unwrap();
1577        sw.add_merge_cell("A1:B1").unwrap();
1578
1579        let (_, original) = sw.into_streamed_data().unwrap();
1580        let cloned = original.try_clone().unwrap();
1581
1582        // Verify metadata is cloned.
1583        assert_eq!(cloned.data_len, original.data_len);
1584        assert_eq!(cloned.cols_xml, original.cols_xml);
1585        assert_eq!(cloned.sheet_views_xml, original.sheet_views_xml);
1586        assert_eq!(cloned.merge_cells_xml, original.merge_cells_xml);
1587
1588        // Verify temp file contents are identical but independent.
1589        assert_ne!(original.temp_file.path(), cloned.temp_file.path());
1590        let orig_bytes = std::fs::read(original.temp_file.path()).unwrap();
1591        let clone_bytes = std::fs::read(cloned.temp_file.path()).unwrap();
1592        assert_eq!(orig_bytes, clone_bytes);
1593        assert!(!orig_bytes.is_empty());
1594    }
1595
1596    #[test]
1597    fn test_date_value() {
1598        let mut sw = StreamWriter::new("Sheet1");
1599        // Excel serial number for 2024-01-15 = 45306
1600        sw.write_row(1, &[CellValue::Date(45306.0)]).unwrap();
1601        let xml = finish_and_get_xml(sw);
1602
1603        // Date is serialized as a plain numeric <v> with no type tag
1604        assert!(xml.contains("<v>45306</v>"));
1605        // Should not have t="..." attribute (same as Number)
1606        assert!(!xml.contains("t=\"inlineStr\""));
1607        assert!(!xml.contains("t=\"b\""));
1608    }
1609
1610    #[test]
1611    fn test_date_value_with_time() {
1612        let mut sw = StreamWriter::new("Sheet1");
1613        // Excel serial with fractional time (noon)
1614        sw.write_row(1, &[CellValue::Date(45306.5)]).unwrap();
1615        let xml = finish_and_get_xml(sw);
1616
1617        assert!(xml.contains("<v>45306.5</v>"));
1618    }
1619
1620    #[test]
1621    fn test_rich_string_to_inline_plain_text() {
1622        use crate::rich_text::RichTextRun;
1623        let mut sw = StreamWriter::new("Sheet1");
1624        sw.write_row(
1625            1,
1626            &[CellValue::RichString(vec![
1627                RichTextRun {
1628                    text: "Hello ".to_string(),
1629                    font: None,
1630                    size: None,
1631                    bold: true,
1632                    italic: false,
1633                    color: None,
1634                },
1635                RichTextRun {
1636                    text: "World".to_string(),
1637                    font: None,
1638                    size: None,
1639                    bold: false,
1640                    italic: true,
1641                    color: None,
1642                },
1643            ])],
1644        )
1645        .unwrap();
1646        let xml = finish_and_get_xml(sw);
1647
1648        // RichString is flattened to plain inline string
1649        assert!(xml.contains("t=\"inlineStr\""));
1650        assert!(xml.contains("<is><t>Hello World</t></is>"));
1651    }
1652
1653    #[test]
1654    fn test_rich_string_xml_escaping() {
1655        use crate::rich_text::RichTextRun;
1656        let mut sw = StreamWriter::new("Sheet1");
1657        sw.write_row(
1658            1,
1659            &[CellValue::RichString(vec![RichTextRun {
1660                text: "A & B <C>".to_string(),
1661                font: None,
1662                size: None,
1663                bold: false,
1664                italic: false,
1665                color: None,
1666            }])],
1667        )
1668        .unwrap();
1669        let xml = finish_and_get_xml(sw);
1670
1671        assert!(xml.contains("A &amp; B &lt;C&gt;"));
1672    }
1673
1674    #[test]
1675    fn test_formula_with_numeric_result() {
1676        let mut sw = StreamWriter::new("Sheet1");
1677        sw.write_row(
1678            1,
1679            &[CellValue::Formula {
1680                expr: "SUM(A2:A10)".to_string(),
1681                result: Some(Box::new(CellValue::Number(55.0))),
1682            }],
1683        )
1684        .unwrap();
1685        let xml = finish_and_get_xml(sw);
1686
1687        assert!(xml.contains("<f>SUM(A2:A10)</f>"));
1688        assert!(xml.contains("<v>55</v>"));
1689        // Numeric results should not have a t attribute (default is numeric)
1690        assert!(!xml.contains("t=\"str\""));
1691        assert!(!xml.contains("t=\"b\""));
1692        assert!(!xml.contains("t=\"e\""));
1693    }
1694
1695    #[test]
1696    fn test_formula_with_error_result() {
1697        let mut sw = StreamWriter::new("Sheet1");
1698        sw.write_row(
1699            1,
1700            &[CellValue::Formula {
1701                expr: "1/0".to_string(),
1702                result: Some(Box::new(CellValue::Error("#DIV/0!".to_string()))),
1703            }],
1704        )
1705        .unwrap();
1706        let xml = finish_and_get_xml(sw);
1707
1708        assert!(xml.contains("t=\"e\""));
1709        assert!(xml.contains("<f>1/0</f>"));
1710        assert!(xml.contains("<v>#DIV/0!</v>"));
1711    }
1712
1713    #[test]
1714    fn test_formula_with_string_result() {
1715        let mut sw = StreamWriter::new("Sheet1");
1716        sw.write_row(
1717            1,
1718            &[CellValue::Formula {
1719                expr: "IF(A1>0,\"yes\",\"no\")".to_string(),
1720                result: Some(Box::new(CellValue::String("yes".to_string()))),
1721            }],
1722        )
1723        .unwrap();
1724        let xml = finish_and_get_xml(sw);
1725
1726        assert!(xml.contains("t=\"str\""));
1727        assert!(xml.contains("<f>"));
1728        assert!(xml.contains("<v>yes</v>"));
1729    }
1730
1731    #[test]
1732    fn test_formula_with_bool_result() {
1733        let mut sw = StreamWriter::new("Sheet1");
1734        sw.write_row(
1735            1,
1736            &[CellValue::Formula {
1737                expr: "A1>0".to_string(),
1738                result: Some(Box::new(CellValue::Bool(true))),
1739            }],
1740        )
1741        .unwrap();
1742        let xml = finish_and_get_xml(sw);
1743
1744        assert!(xml.contains("t=\"b\""));
1745        assert!(xml.contains("<f>A1&gt;0</f>"));
1746        assert!(xml.contains("<v>1</v>"));
1747    }
1748
1749    #[test]
1750    fn test_formula_without_result() {
1751        let mut sw = StreamWriter::new("Sheet1");
1752        sw.write_row(
1753            1,
1754            &[CellValue::Formula {
1755                expr: "A1+B1".to_string(),
1756                result: None,
1757            }],
1758        )
1759        .unwrap();
1760        let xml = finish_and_get_xml(sw);
1761
1762        assert!(xml.contains("<f>A1+B1</f></c>"));
1763        // No <v> tag when result is None
1764        assert!(!xml.contains("<v>"));
1765    }
1766
1767    #[test]
1768    fn test_error_value_xml_escaping() {
1769        let mut sw = StreamWriter::new("Sheet1");
1770        sw.write_row(1, &[CellValue::Error("#VALUE!".to_string())])
1771            .unwrap();
1772        let xml = finish_and_get_xml(sw);
1773
1774        assert!(xml.contains("t=\"e\""));
1775        assert!(xml.contains("#VALUE!"));
1776    }
1777
1778    #[test]
1779    fn test_empty_row_produces_valid_xml() {
1780        let mut sw = StreamWriter::new("Sheet1");
1781        // Row with only empty values
1782        sw.write_row(1, &[CellValue::Empty, CellValue::Empty])
1783            .unwrap();
1784        let ws = finish_and_parse(sw);
1785
1786        assert_eq!(ws.sheet_data.rows.len(), 1);
1787        assert_eq!(ws.sheet_data.rows[0].r, 1);
1788        // All empties are skipped, row has no cells
1789        assert_eq!(ws.sheet_data.rows[0].cells.len(), 0);
1790    }
1791
1792    #[test]
1793    fn test_memory_efficiency() {
1794        // This test validates that large data doesn't accumulate in memory.
1795        // The StreamWriter writes to a temp file, so memory should remain
1796        // roughly constant regardless of row count.
1797        let mut sw = StreamWriter::new("Sheet1");
1798        for i in 1..=100_000u32 {
1799            sw.write_row(
1800                i,
1801                &[
1802                    CellValue::from(format!("Row number {}", i)),
1803                    CellValue::from(i as f64),
1804                ],
1805            )
1806            .unwrap();
1807        }
1808        // Verify substantial data was written to disk, not held in memory.
1809        assert!(sw.bytes_written > 1_000_000);
1810
1811        let (_, streamed) = sw.into_streamed_data().unwrap();
1812        assert!(streamed.data_len > 1_000_000);
1813    }
1814
1815    #[test]
1816    fn test_write_rows_basic() {
1817        let mut sw = StreamWriter::new("Sheet1");
1818        sw.write_rows(
1819            1,
1820            &[
1821                vec![CellValue::from("A"), CellValue::from(1)],
1822                vec![CellValue::from("B"), CellValue::from(2)],
1823                vec![CellValue::from("C"), CellValue::from(3)],
1824            ],
1825        )
1826        .unwrap();
1827        let ws = finish_and_parse(sw);
1828
1829        assert_eq!(ws.sheet_data.rows.len(), 3);
1830        assert_eq!(ws.sheet_data.rows[0].r, 1);
1831        assert_eq!(ws.sheet_data.rows[1].r, 2);
1832        assert_eq!(ws.sheet_data.rows[2].r, 3);
1833        assert_eq!(ws.sheet_data.rows[0].cells.len(), 2);
1834        assert_eq!(ws.sheet_data.rows[0].cells[0].r, "A1");
1835        assert_eq!(ws.sheet_data.rows[2].cells[0].r, "A3");
1836    }
1837
1838    #[test]
1839    fn test_write_rows_empty() {
1840        let mut sw = StreamWriter::new("Sheet1");
1841        sw.write_rows(1, &[]).unwrap();
1842        let ws = finish_and_parse(sw);
1843
1844        assert!(ws.sheet_data.rows.is_empty());
1845    }
1846
1847    #[test]
1848    fn test_write_rows_mixed_with_single() {
1849        let mut sw = StreamWriter::new("Sheet1");
1850        sw.write_row(1, &[CellValue::from("first")]).unwrap();
1851        sw.write_rows(
1852            2,
1853            &[
1854                vec![CellValue::from("second")],
1855                vec![CellValue::from("third")],
1856                vec![CellValue::from("fourth")],
1857            ],
1858        )
1859        .unwrap();
1860        let ws = finish_and_parse(sw);
1861
1862        assert_eq!(ws.sheet_data.rows.len(), 4);
1863        assert_eq!(ws.sheet_data.rows[0].r, 1);
1864        assert_eq!(ws.sheet_data.rows[1].r, 2);
1865        assert_eq!(ws.sheet_data.rows[2].r, 3);
1866        assert_eq!(ws.sheet_data.rows[3].r, 4);
1867    }
1868
1869    #[test]
1870    fn test_write_rows_overflow() {
1871        let mut sw = StreamWriter::new("Sheet1");
1872        sw.write_row(1, &[CellValue::from("ok")]).unwrap();
1873        let result = sw.write_rows(
1874            u32::MAX - 1,
1875            &[
1876                vec![CellValue::from("a")],
1877                vec![CellValue::from("b")],
1878                vec![CellValue::from("c")],
1879            ],
1880        );
1881        assert!(result.is_err());
1882    }
1883
1884    #[test]
1885    fn test_write_rows_backward_error() {
1886        let mut sw = StreamWriter::new("Sheet1");
1887        sw.write_row(5, &[CellValue::from("row5")]).unwrap();
1888        let result = sw.write_rows(3, &[vec![CellValue::from("a")], vec![CellValue::from("b")]]);
1889        assert!(result.is_err());
1890        assert!(matches!(
1891            result.unwrap_err(),
1892            Error::StreamRowAlreadyWritten { row: 3 }
1893        ));
1894    }
1895}