sheetkit_core/workbook/
sheet_ops.rs

1use super::*;
2
3impl Workbook {
4    /// Return the names of all sheets in workbook order.
5    pub fn sheet_names(&self) -> Vec<&str> {
6        self.worksheets
7            .iter()
8            .map(|(name, _)| name.as_str())
9            .collect()
10    }
11
12    /// Create a new empty sheet with the given name. Returns the 0-based sheet index.
13    pub fn new_sheet(&mut self, name: &str) -> Result<usize> {
14        let idx = crate::sheet::add_sheet(
15            &mut self.workbook_xml,
16            &mut self.workbook_rels,
17            &mut self.content_types,
18            &mut self.worksheets,
19            name,
20            WorksheetXml::default(),
21        )?;
22        if self.sheet_comments.len() < self.worksheets.len() {
23            self.sheet_comments.push(None);
24        }
25        if self.sheet_sparklines.len() < self.worksheets.len() {
26            self.sheet_sparklines.push(vec![]);
27        }
28        if self.sheet_vml.len() < self.worksheets.len() {
29            self.sheet_vml.push(None);
30        }
31        if self.raw_sheet_xml.len() < self.worksheets.len() {
32            self.raw_sheet_xml.push(None);
33        }
34        if self.sheet_dirty.len() < self.worksheets.len() {
35            self.sheet_dirty.push(true);
36        }
37        if self.sheet_threaded_comments.len() < self.worksheets.len() {
38            self.sheet_threaded_comments.push(None);
39        }
40        if self.sheet_form_controls.len() < self.worksheets.len() {
41            self.sheet_form_controls.push(vec![]);
42        }
43        self.rebuild_sheet_index();
44        Ok(idx)
45    }
46
47    /// Delete a sheet by name.
48    pub fn delete_sheet(&mut self, name: &str) -> Result<()> {
49        let idx = self.sheet_index(name)?;
50        self.assert_parallel_vecs_in_sync();
51
52        crate::sheet::delete_sheet(
53            &mut self.workbook_xml,
54            &mut self.workbook_rels,
55            &mut self.content_types,
56            &mut self.worksheets,
57            name,
58        )?;
59
60        // Remove all per-sheet parallel data at once. After delete_sheet
61        // above, worksheets has already been shortened by 1 so these
62        // vectors must follow.
63        self.sheet_comments.remove(idx);
64        self.sheet_sparklines.remove(idx);
65        self.sheet_vml.remove(idx);
66        self.raw_sheet_xml.remove(idx);
67        self.sheet_dirty.remove(idx);
68        self.sheet_threaded_comments.remove(idx);
69        self.sheet_form_controls.remove(idx);
70
71        // Remove tables belonging to the deleted sheet and re-index remaining.
72        self.tables.retain(|(_, _, si)| *si != idx);
73        for (_, _, si) in &mut self.tables {
74            if *si > idx {
75                *si -= 1;
76            }
77        }
78
79        // Remove and reindex streamed sheet data.
80        self.streamed_sheets.remove(&idx);
81        self.streamed_sheets = self
82            .streamed_sheets
83            .drain()
84            .map(|(i, data)| if i > idx { (i - 1, data) } else { (i, data) })
85            .collect();
86
87        self.reindex_sheet_maps_after_delete(idx);
88        self.rebuild_sheet_index();
89        Ok(())
90    }
91
92    /// Debug assertion that all per-sheet parallel vectors have the same
93    /// length as `worksheets`. Catching desync early prevents silent data
94    /// corruption from mismatched indices.
95    fn assert_parallel_vecs_in_sync(&self) {
96        let n = self.worksheets.len();
97        debug_assert_eq!(self.sheet_comments.len(), n, "sheet_comments desync");
98        debug_assert_eq!(self.sheet_sparklines.len(), n, "sheet_sparklines desync");
99        debug_assert_eq!(self.sheet_vml.len(), n, "sheet_vml desync");
100        debug_assert_eq!(self.raw_sheet_xml.len(), n, "raw_sheet_xml desync");
101        debug_assert_eq!(self.sheet_dirty.len(), n, "sheet_dirty desync");
102        debug_assert_eq!(
103            self.sheet_threaded_comments.len(),
104            n,
105            "sheet_threaded_comments desync"
106        );
107        debug_assert_eq!(
108            self.sheet_form_controls.len(),
109            n,
110            "sheet_form_controls desync"
111        );
112    }
113
114    /// Rename a sheet.
115    pub fn set_sheet_name(&mut self, old_name: &str, new_name: &str) -> Result<()> {
116        crate::sheet::rename_sheet(
117            &mut self.workbook_xml,
118            &mut self.worksheets,
119            old_name,
120            new_name,
121        )?;
122        self.rebuild_sheet_index();
123        Ok(())
124    }
125
126    /// Copy a sheet, returning the 0-based index of the new copy.
127    pub fn copy_sheet(&mut self, source: &str, target: &str) -> Result<usize> {
128        // Resolve the source index before copy_sheet changes the array.
129        let src_idx = self.sheet_index(source)?;
130        // Hydrate the source sheet so copy_sheet clones the real data,
131        // not an empty default.
132        self.ensure_hydrated(src_idx)?;
133        let idx = crate::sheet::copy_sheet(
134            &mut self.workbook_xml,
135            &mut self.workbook_rels,
136            &mut self.content_types,
137            &mut self.worksheets,
138            source,
139            target,
140        )?;
141        if self.sheet_comments.len() < self.worksheets.len() {
142            self.sheet_comments.push(None);
143        }
144        let source_sparklines = self
145            .sheet_sparklines
146            .get(src_idx)
147            .cloned()
148            .unwrap_or_default();
149        if self.sheet_sparklines.len() < self.worksheets.len() {
150            self.sheet_sparklines.push(source_sparklines);
151        }
152        if self.sheet_vml.len() < self.worksheets.len() {
153            self.sheet_vml.push(None);
154        }
155        if self.raw_sheet_xml.len() < self.worksheets.len() {
156            self.raw_sheet_xml.push(None);
157        }
158        if self.sheet_dirty.len() < self.worksheets.len() {
159            self.sheet_dirty.push(true);
160        }
161        if self.sheet_threaded_comments.len() < self.worksheets.len() {
162            self.sheet_threaded_comments.push(None);
163        }
164        if self.sheet_form_controls.len() < self.worksheets.len() {
165            self.sheet_form_controls.push(vec![]);
166        }
167        // Copy streamed data if the source sheet was streamed.
168        if let Some(src_streamed) = self.streamed_sheets.get(&src_idx) {
169            let cloned = src_streamed.try_clone()?;
170            self.streamed_sheets.insert(idx, cloned);
171        }
172        self.rebuild_sheet_index();
173        Ok(idx)
174    }
175
176    /// Get a sheet's 0-based index by name. Returns `None` if not found.
177    pub fn get_sheet_index(&self, name: &str) -> Option<usize> {
178        crate::sheet::find_sheet_index(&self.worksheets, name)
179    }
180
181    /// Get the name of the active sheet.
182    pub fn get_active_sheet(&self) -> &str {
183        let idx = crate::sheet::active_sheet_index(&self.workbook_xml);
184        self.worksheets
185            .get(idx)
186            .map(|(n, _)| n.as_str())
187            .unwrap_or_else(|| self.worksheets[0].0.as_str())
188    }
189
190    /// Set the active sheet by name.
191    pub fn set_active_sheet(&mut self, name: &str) -> Result<()> {
192        let idx = crate::sheet::find_sheet_index(&self.worksheets, name).ok_or_else(|| {
193            Error::SheetNotFound {
194                name: name.to_string(),
195            }
196        })?;
197        crate::sheet::set_active_sheet_index(&mut self.workbook_xml, idx as u32);
198        Ok(())
199    }
200
201    /// Create a [`StreamWriter`](crate::stream::StreamWriter) for a new sheet.
202    ///
203    /// The sheet will be added to the workbook when the StreamWriter is applied
204    /// via [`apply_stream_writer`](Self::apply_stream_writer).
205    pub fn new_stream_writer(&self, sheet_name: &str) -> Result<crate::stream::StreamWriter> {
206        crate::sheet::validate_sheet_name(sheet_name)?;
207        if self.worksheets.iter().any(|(n, _)| n == sheet_name) {
208            return Err(Error::SheetAlreadyExists {
209                name: sheet_name.to_string(),
210            });
211        }
212        Ok(crate::stream::StreamWriter::new(sheet_name))
213    }
214
215    /// Apply a completed [`StreamWriter`](crate::stream::StreamWriter) to the
216    /// workbook, adding it as a new sheet.
217    ///
218    /// The streamed row data stays on disk (in a temp file) and is written
219    /// directly to the ZIP archive during save, keeping memory usage constant
220    /// regardless of the number of rows.
221    ///
222    /// **Note:** Cell values in streamed sheets cannot be read back via
223    /// [`get_cell_value`](Self::get_cell_value) before saving. Save the
224    /// workbook and reopen it to read the data.
225    ///
226    /// Returns the 0-based index of the new sheet.
227    pub fn apply_stream_writer(&mut self, writer: crate::stream::StreamWriter) -> Result<usize> {
228        let (sheet_name, streamed_data) = writer.into_streamed_data()?;
229
230        // Add an empty WorksheetXml placeholder for sheet management
231        // (sheet names, indices, metadata). The actual data lives in the
232        // temp file and is streamed to the ZIP during save.
233        let idx = crate::sheet::add_sheet(
234            &mut self.workbook_xml,
235            &mut self.workbook_rels,
236            &mut self.content_types,
237            &mut self.worksheets,
238            &sheet_name,
239            WorksheetXml::default(),
240        )?;
241        if self.sheet_comments.len() < self.worksheets.len() {
242            self.sheet_comments.push(None);
243        }
244        if self.sheet_sparklines.len() < self.worksheets.len() {
245            self.sheet_sparklines.push(vec![]);
246        }
247        if self.sheet_vml.len() < self.worksheets.len() {
248            self.sheet_vml.push(None);
249        }
250        if self.raw_sheet_xml.len() < self.worksheets.len() {
251            self.raw_sheet_xml.push(None);
252        }
253        if self.sheet_dirty.len() < self.worksheets.len() {
254            self.sheet_dirty.push(true);
255        }
256        if self.sheet_threaded_comments.len() < self.worksheets.len() {
257            self.sheet_threaded_comments.push(None);
258        }
259        if self.sheet_form_controls.len() < self.worksheets.len() {
260            self.sheet_form_controls.push(vec![]);
261        }
262
263        // Store the streamed data for use during save.
264        self.streamed_sheets.insert(idx, streamed_data);
265
266        self.rebuild_sheet_index();
267        Ok(idx)
268    }
269
270    /// Insert `count` empty rows starting at `start_row` in the named sheet.
271    pub fn insert_rows(&mut self, sheet: &str, start_row: u32, count: u32) -> Result<()> {
272        let sheet_idx = self.sheet_index(sheet)?;
273        {
274            let ws = self.worksheet_mut_by_index(sheet_idx)?;
275            crate::row::insert_rows(ws, start_row, count)?;
276        }
277        self.apply_reference_shift_for_sheet(sheet_idx, |col, row| {
278            if row >= start_row {
279                (col, row + count)
280            } else {
281                (col, row)
282            }
283        })
284    }
285
286    /// Remove a single row from the named sheet, shifting rows below it up.
287    pub fn remove_row(&mut self, sheet: &str, row: u32) -> Result<()> {
288        let sheet_idx = self.sheet_index(sheet)?;
289        {
290            let ws = self.worksheet_mut_by_index(sheet_idx)?;
291            crate::row::remove_row(ws, row)?;
292        }
293        self.apply_reference_shift_for_sheet(sheet_idx, |col, r| {
294            if r > row {
295                (col, r - 1)
296            } else {
297                (col, r)
298            }
299        })
300    }
301
302    /// Duplicate a row, inserting the copy directly below.
303    pub fn duplicate_row(&mut self, sheet: &str, row: u32) -> Result<()> {
304        let ws = self.worksheet_mut(sheet)?;
305        crate::row::duplicate_row(ws, row)
306    }
307
308    /// Set the height of a row in points.
309    pub fn set_row_height(&mut self, sheet: &str, row: u32, height: f64) -> Result<()> {
310        let ws = self.worksheet_mut(sheet)?;
311        crate::row::set_row_height(ws, row, height)
312    }
313
314    /// Get the height of a row.
315    pub fn get_row_height(&self, sheet: &str, row: u32) -> Result<Option<f64>> {
316        let ws = self.worksheet_ref(sheet)?;
317        Ok(crate::row::get_row_height(ws, row))
318    }
319
320    /// Set the visibility of a row.
321    pub fn set_row_visible(&mut self, sheet: &str, row: u32, visible: bool) -> Result<()> {
322        let ws = self.worksheet_mut(sheet)?;
323        crate::row::set_row_visible(ws, row, visible)
324    }
325
326    /// Get the visibility of a row. Returns true if visible (not hidden).
327    pub fn get_row_visible(&self, sheet: &str, row: u32) -> Result<bool> {
328        let ws = self.worksheet_ref(sheet)?;
329        Ok(crate::row::get_row_visible(ws, row))
330    }
331
332    /// Set the outline level of a row.
333    pub fn set_row_outline_level(&mut self, sheet: &str, row: u32, level: u8) -> Result<()> {
334        let ws = self.worksheet_mut(sheet)?;
335        crate::row::set_row_outline_level(ws, row, level)
336    }
337
338    /// Get the outline level of a row. Returns 0 if not set.
339    pub fn get_row_outline_level(&self, sheet: &str, row: u32) -> Result<u8> {
340        let ws = self.worksheet_ref(sheet)?;
341        Ok(crate::row::get_row_outline_level(ws, row))
342    }
343
344    /// Set the style for an entire row.
345    ///
346    /// The `style_id` must be a valid index in cellXfs (returned by `add_style`).
347    pub fn set_row_style(&mut self, sheet: &str, row: u32, style_id: u32) -> Result<()> {
348        if style_id as usize >= self.stylesheet.cell_xfs.xfs.len() {
349            return Err(Error::StyleNotFound { id: style_id });
350        }
351        let ws = self.worksheet_mut(sheet)?;
352        crate::row::set_row_style(ws, row, style_id)
353    }
354
355    /// Get the style ID for a row. Returns 0 (default) if not set.
356    pub fn get_row_style(&self, sheet: &str, row: u32) -> Result<u32> {
357        let ws = self.worksheet_ref(sheet)?;
358        Ok(crate::row::get_row_style(ws, row))
359    }
360
361    /// Get all rows with their data from a sheet.
362    ///
363    /// Returns a Vec of `(row_number, Vec<(column_number, CellValue)>)` tuples.
364    /// Column numbers are 1-based (A=1, B=2, ...). Only rows that contain at
365    /// least one cell are included (sparse).
366    #[allow(clippy::type_complexity)]
367    pub fn get_rows(&self, sheet: &str) -> Result<Vec<(u32, Vec<(u32, CellValue)>)>> {
368        let ws = self.worksheet_ref(sheet)?;
369        crate::row::get_rows(ws, &self.sst_runtime)
370    }
371
372    /// Get all columns with their data from a sheet.
373    ///
374    /// Returns a Vec of `(column_name, Vec<(row_number, CellValue)>)` tuples.
375    /// Only columns that have data are included (sparse).
376    #[allow(clippy::type_complexity)]
377    pub fn get_cols(&self, sheet: &str) -> Result<Vec<(String, Vec<(u32, CellValue)>)>> {
378        let ws = self.worksheet_ref(sheet)?;
379        crate::col::get_cols(ws, &self.sst_runtime)
380    }
381
382    /// Set the width of a column.
383    pub fn set_col_width(&mut self, sheet: &str, col: &str, width: f64) -> Result<()> {
384        let ws = self.worksheet_mut(sheet)?;
385        crate::col::set_col_width(ws, col, width)
386    }
387
388    /// Get the width of a column.
389    pub fn get_col_width(&self, sheet: &str, col: &str) -> Result<Option<f64>> {
390        let ws = self.worksheet_ref(sheet)?;
391        Ok(crate::col::get_col_width(ws, col))
392    }
393
394    /// Set the visibility of a column.
395    pub fn set_col_visible(&mut self, sheet: &str, col: &str, visible: bool) -> Result<()> {
396        let ws = self.worksheet_mut(sheet)?;
397        crate::col::set_col_visible(ws, col, visible)
398    }
399
400    /// Get the visibility of a column. Returns true if visible (not hidden).
401    pub fn get_col_visible(&self, sheet: &str, col: &str) -> Result<bool> {
402        let ws = self.worksheet_ref(sheet)?;
403        crate::col::get_col_visible(ws, col)
404    }
405
406    /// Set the outline level of a column.
407    pub fn set_col_outline_level(&mut self, sheet: &str, col: &str, level: u8) -> Result<()> {
408        let ws = self.worksheet_mut(sheet)?;
409        crate::col::set_col_outline_level(ws, col, level)
410    }
411
412    /// Get the outline level of a column. Returns 0 if not set.
413    pub fn get_col_outline_level(&self, sheet: &str, col: &str) -> Result<u8> {
414        let ws = self.worksheet_ref(sheet)?;
415        crate::col::get_col_outline_level(ws, col)
416    }
417
418    /// Set the style for an entire column.
419    ///
420    /// The `style_id` must be a valid index in cellXfs (returned by `add_style`).
421    pub fn set_col_style(&mut self, sheet: &str, col: &str, style_id: u32) -> Result<()> {
422        if style_id as usize >= self.stylesheet.cell_xfs.xfs.len() {
423            return Err(Error::StyleNotFound { id: style_id });
424        }
425        let ws = self.worksheet_mut(sheet)?;
426        crate::col::set_col_style(ws, col, style_id)
427    }
428
429    /// Get the style ID for a column. Returns 0 (default) if not set.
430    pub fn get_col_style(&self, sheet: &str, col: &str) -> Result<u32> {
431        let ws = self.worksheet_ref(sheet)?;
432        crate::col::get_col_style(ws, col)
433    }
434
435    /// Insert `count` columns starting at `col` in the named sheet.
436    pub fn insert_cols(&mut self, sheet: &str, col: &str, count: u32) -> Result<()> {
437        let sheet_idx = self.sheet_index(sheet)?;
438        let start_col = column_name_to_number(col)?;
439        {
440            let ws = self.worksheet_mut_by_index(sheet_idx)?;
441            crate::col::insert_cols(ws, col, count)?;
442        }
443        self.apply_reference_shift_for_sheet(sheet_idx, |c, row| {
444            if c >= start_col {
445                (c + count, row)
446            } else {
447                (c, row)
448            }
449        })
450    }
451
452    /// Remove a single column from the named sheet.
453    pub fn remove_col(&mut self, sheet: &str, col: &str) -> Result<()> {
454        let sheet_idx = self.sheet_index(sheet)?;
455        let col_num = column_name_to_number(col)?;
456        {
457            let ws = self.worksheet_mut_by_index(sheet_idx)?;
458            crate::col::remove_col(ws, col)?;
459        }
460        self.apply_reference_shift_for_sheet(sheet_idx, |c, row| {
461            if c > col_num {
462                (c - 1, row)
463            } else {
464                (c, row)
465            }
466        })
467    }
468
469    /// Reindex per-sheet maps after deleting a sheet.
470    pub(crate) fn reindex_sheet_maps_after_delete(&mut self, removed_idx: usize) {
471        self.worksheet_rels = self
472            .worksheet_rels
473            .iter()
474            .filter_map(|(idx, rels)| {
475                if *idx == removed_idx {
476                    None
477                } else if *idx > removed_idx {
478                    Some((idx - 1, rels.clone()))
479                } else {
480                    Some((*idx, rels.clone()))
481                }
482            })
483            .collect();
484
485        self.worksheet_drawings = self
486            .worksheet_drawings
487            .iter()
488            .filter_map(|(idx, drawing_idx)| {
489                if *idx == removed_idx {
490                    None
491                } else if *idx > removed_idx {
492                    Some((idx - 1, *drawing_idx))
493                } else {
494                    Some((*idx, *drawing_idx))
495                }
496            })
497            .collect();
498    }
499
500    /// Apply a cell-reference shift transformation to sheet-scoped structures.
501    pub(crate) fn apply_reference_shift_for_sheet<F>(
502        &mut self,
503        sheet_idx: usize,
504        shift_cell: F,
505    ) -> Result<()>
506    where
507        F: Fn(u32, u32) -> (u32, u32) + Copy,
508    {
509        {
510            let ws = self.worksheet_mut_by_index(sheet_idx)?;
511
512            // Cell formulas.
513            for row in &mut ws.sheet_data.rows {
514                for cell in &mut row.cells {
515                    if let Some(ref mut f) = cell.f {
516                        if let Some(ref mut expr) = f.value {
517                            *expr = shift_cell_references_in_text(expr, shift_cell)?;
518                        }
519                    }
520                }
521            }
522
523            // Merged ranges.
524            if let Some(ref mut merges) = ws.merge_cells {
525                for mc in &mut merges.merge_cells {
526                    mc.reference = shift_cell_references_in_text(&mc.reference, shift_cell)?;
527                }
528                // Invalidate the coordinate cache since references changed.
529                merges.cached_coords.clear();
530            }
531
532            // Auto-filter.
533            if let Some(ref mut af) = ws.auto_filter {
534                af.reference = shift_cell_references_in_text(&af.reference, shift_cell)?;
535            }
536
537            // Data validations.
538            if let Some(ref mut dvs) = ws.data_validations {
539                for dv in &mut dvs.data_validations {
540                    dv.sqref = shift_cell_references_in_text(&dv.sqref, shift_cell)?;
541                    if let Some(ref mut f1) = dv.formula1 {
542                        *f1 = shift_cell_references_in_text(f1, shift_cell)?;
543                    }
544                    if let Some(ref mut f2) = dv.formula2 {
545                        *f2 = shift_cell_references_in_text(f2, shift_cell)?;
546                    }
547                }
548            }
549
550            // Conditional formatting ranges/formulas.
551            for cf in &mut ws.conditional_formatting {
552                cf.sqref = shift_cell_references_in_text(&cf.sqref, shift_cell)?;
553                for rule in &mut cf.cf_rules {
554                    for f in &mut rule.formulas {
555                        *f = shift_cell_references_in_text(f, shift_cell)?;
556                    }
557                }
558            }
559
560            // Hyperlinks.
561            if let Some(ref mut hyperlinks) = ws.hyperlinks {
562                for hl in &mut hyperlinks.hyperlinks {
563                    hl.reference = shift_cell_references_in_text(&hl.reference, shift_cell)?;
564                    if let Some(ref mut loc) = hl.location {
565                        *loc = shift_cell_references_in_text(loc, shift_cell)?;
566                    }
567                }
568            }
569
570            // Pane/selection references.
571            if let Some(ref mut views) = ws.sheet_views {
572                for view in &mut views.sheet_views {
573                    if let Some(ref mut pane) = view.pane {
574                        if let Some(ref mut top_left) = pane.top_left_cell {
575                            *top_left = shift_cell_references_in_text(top_left, shift_cell)?;
576                        }
577                    }
578                    for sel in &mut view.selection {
579                        if let Some(ref mut ac) = sel.active_cell {
580                            *ac = shift_cell_references_in_text(ac, shift_cell)?;
581                        }
582                        if let Some(ref mut sqref) = sel.sqref {
583                            *sqref = shift_cell_references_in_text(sqref, shift_cell)?;
584                        }
585                    }
586                }
587            }
588        }
589
590        // Drawing anchors attached to this sheet.
591        if let Some(&drawing_idx) = self.worksheet_drawings.get(&sheet_idx) {
592            if let Some((_, drawing)) = self.drawings.get_mut(drawing_idx) {
593                for anchor in &mut drawing.one_cell_anchors {
594                    let (new_col, new_row) = shift_cell(anchor.from.col + 1, anchor.from.row + 1);
595                    anchor.from.col = new_col - 1;
596                    anchor.from.row = new_row - 1;
597                }
598                for anchor in &mut drawing.two_cell_anchors {
599                    let (from_col, from_row) = shift_cell(anchor.from.col + 1, anchor.from.row + 1);
600                    anchor.from.col = from_col - 1;
601                    anchor.from.row = from_row - 1;
602                    let (to_col, to_row) = shift_cell(anchor.to.col + 1, anchor.to.row + 1);
603                    anchor.to.col = to_col - 1;
604                    anchor.to.row = to_row - 1;
605                }
606            }
607        }
608
609        Ok(())
610    }
611
612    /// Ensure a drawing exists for the given sheet index, creating one if needed.
613    /// Returns the drawing index.
614    pub(crate) fn ensure_drawing_for_sheet(&mut self, sheet_idx: usize) -> usize {
615        if let Some(&idx) = self.worksheet_drawings.get(&sheet_idx) {
616            return idx;
617        }
618
619        let idx = self.drawings.len();
620        let drawing_path = format!("xl/drawings/drawing{}.xml", idx + 1);
621        self.drawings.push((drawing_path, WsDr::default()));
622        self.worksheet_drawings.insert(sheet_idx, idx);
623
624        // Add drawing reference to the worksheet.
625        let ws_rid = self.next_worksheet_rid(sheet_idx);
626        // ensure_hydrated can only fail if the sheet was never loaded, which
627        // should not happen for a sheet we're actively attaching a drawing to.
628        // Use expect instead of `?` because this method returns `usize`.
629        self.ensure_hydrated(sheet_idx)
630            .expect("sheet must be hydrated before attaching a drawing");
631        self.mark_sheet_dirty(sheet_idx);
632        self.worksheets[sheet_idx].1.get_mut().unwrap().drawing = Some(DrawingRef {
633            r_id: ws_rid.clone(),
634        });
635
636        // Add worksheet->drawing relationship.
637        let drawing_rel_target = format!("../drawings/drawing{}.xml", idx + 1);
638        let ws_rels = self
639            .worksheet_rels
640            .entry(sheet_idx)
641            .or_insert_with(|| Relationships {
642                xmlns: sheetkit_xml::namespaces::PACKAGE_RELATIONSHIPS.to_string(),
643                relationships: vec![],
644            });
645        ws_rels.relationships.push(Relationship {
646            id: ws_rid,
647            rel_type: rel_types::DRAWING.to_string(),
648            target: drawing_rel_target,
649            target_mode: None,
650        });
651
652        // Add content type for the drawing.
653        self.content_types.overrides.push(ContentTypeOverride {
654            part_name: format!("/xl/drawings/drawing{}.xml", idx + 1),
655            content_type: mime_types::DRAWING.to_string(),
656        });
657
658        idx
659    }
660
661    /// Generate the next relationship ID for a worksheet's rels.
662    pub(crate) fn next_worksheet_rid(&self, sheet_idx: usize) -> String {
663        let existing = self
664            .worksheet_rels
665            .get(&sheet_idx)
666            .map(|r| r.relationships.as_slice())
667            .unwrap_or(&[]);
668        crate::sheet::next_rid(existing)
669    }
670
671    /// Generate the next relationship ID for a drawing's rels.
672    pub(crate) fn next_drawing_rid(&self, drawing_idx: usize) -> String {
673        let existing = self
674            .drawing_rels
675            .get(&drawing_idx)
676            .map(|r| r.relationships.as_slice())
677            .unwrap_or(&[]);
678        crate::sheet::next_rid(existing)
679    }
680}
681
682#[cfg(test)]
683#[allow(clippy::approx_constant)]
684mod tests {
685    use super::*;
686    use tempfile::TempDir;
687
688    #[test]
689    fn test_new_sheet_basic() {
690        let mut wb = Workbook::new();
691        let idx = wb.new_sheet("Sheet2").unwrap();
692        assert_eq!(idx, 1);
693        assert_eq!(wb.sheet_names(), vec!["Sheet1", "Sheet2"]);
694    }
695
696    #[test]
697    fn test_new_sheet_duplicate_returns_error() {
698        let mut wb = Workbook::new();
699        let result = wb.new_sheet("Sheet1");
700        assert!(result.is_err());
701        assert!(matches!(
702            result.unwrap_err(),
703            Error::SheetAlreadyExists { .. }
704        ));
705    }
706
707    #[test]
708    fn test_new_sheet_invalid_name_returns_error() {
709        let mut wb = Workbook::new();
710        let result = wb.new_sheet("Bad/Name");
711        assert!(result.is_err());
712        assert!(matches!(result.unwrap_err(), Error::InvalidSheetName(_)));
713    }
714
715    #[test]
716    fn test_delete_sheet_basic() {
717        let mut wb = Workbook::new();
718        wb.new_sheet("Sheet2").unwrap();
719        wb.delete_sheet("Sheet1").unwrap();
720        assert_eq!(wb.sheet_names(), vec!["Sheet2"]);
721    }
722
723    #[test]
724    fn test_delete_sheet_keeps_parallel_vecs_in_sync() {
725        let mut wb = Workbook::new();
726        wb.new_sheet("Sheet2").unwrap();
727        wb.new_sheet("Sheet3").unwrap();
728
729        // Add comments to Sheet2 (middle sheet).
730        wb.add_comment(
731            "Sheet2",
732            &crate::comment::CommentConfig {
733                cell: "A1".to_string(),
734                author: "Test".to_string(),
735                text: "note".to_string(),
736            },
737        )
738        .unwrap();
739
740        // Delete the middle sheet and verify no panic.
741        wb.delete_sheet("Sheet2").unwrap();
742        assert_eq!(wb.sheet_names(), vec!["Sheet1", "Sheet3"]);
743
744        // After deletion, adding a comment to Sheet3 (now index 1)
745        // should work without index mismatch.
746        wb.add_comment(
747            "Sheet3",
748            &crate::comment::CommentConfig {
749                cell: "B2".to_string(),
750                author: "Test".to_string(),
751                text: "note2".to_string(),
752            },
753        )
754        .unwrap();
755    }
756
757    #[test]
758    fn test_delete_last_sheet_returns_error() {
759        let mut wb = Workbook::new();
760        let result = wb.delete_sheet("Sheet1");
761        assert!(result.is_err());
762    }
763
764    #[test]
765    fn test_delete_nonexistent_sheet_returns_error() {
766        let mut wb = Workbook::new();
767        let result = wb.delete_sheet("NoSuchSheet");
768        assert!(result.is_err());
769        assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
770    }
771
772    #[test]
773    fn test_set_sheet_name_basic() {
774        let mut wb = Workbook::new();
775        wb.set_sheet_name("Sheet1", "Renamed").unwrap();
776        assert_eq!(wb.sheet_names(), vec!["Renamed"]);
777    }
778
779    #[test]
780    fn test_set_sheet_name_to_existing_returns_error() {
781        let mut wb = Workbook::new();
782        wb.new_sheet("Sheet2").unwrap();
783        let result = wb.set_sheet_name("Sheet1", "Sheet2");
784        assert!(result.is_err());
785        assert!(matches!(
786            result.unwrap_err(),
787            Error::SheetAlreadyExists { .. }
788        ));
789    }
790
791    #[test]
792    fn test_copy_sheet_basic() {
793        let mut wb = Workbook::new();
794        let idx = wb.copy_sheet("Sheet1", "Sheet1 Copy").unwrap();
795        assert_eq!(idx, 1);
796        assert_eq!(wb.sheet_names(), vec!["Sheet1", "Sheet1 Copy"]);
797    }
798
799    #[test]
800    fn test_get_sheet_index() {
801        let mut wb = Workbook::new();
802        wb.new_sheet("Sheet2").unwrap();
803        assert_eq!(wb.get_sheet_index("Sheet1"), Some(0));
804        assert_eq!(wb.get_sheet_index("Sheet2"), Some(1));
805        assert_eq!(wb.get_sheet_index("Nonexistent"), None);
806    }
807
808    #[test]
809    fn test_get_active_sheet_default() {
810        let wb = Workbook::new();
811        assert_eq!(wb.get_active_sheet(), "Sheet1");
812    }
813
814    #[test]
815    fn test_set_active_sheet() {
816        let mut wb = Workbook::new();
817        wb.new_sheet("Sheet2").unwrap();
818        wb.set_active_sheet("Sheet2").unwrap();
819        assert_eq!(wb.get_active_sheet(), "Sheet2");
820    }
821
822    #[test]
823    fn test_set_active_sheet_not_found() {
824        let mut wb = Workbook::new();
825        let result = wb.set_active_sheet("NoSuchSheet");
826        assert!(result.is_err());
827        assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
828    }
829
830    #[test]
831    fn test_sheet_management_roundtrip_save_open() {
832        let dir = TempDir::new().unwrap();
833        let path = dir.path().join("sheet_mgmt.xlsx");
834
835        let mut wb = Workbook::new();
836        wb.new_sheet("Data").unwrap();
837        wb.new_sheet("Summary").unwrap();
838        wb.set_sheet_name("Sheet1", "Overview").unwrap();
839        wb.save(&path).unwrap();
840
841        let wb2 = Workbook::open(&path).unwrap();
842        assert_eq!(wb2.sheet_names(), vec!["Overview", "Data", "Summary"]);
843    }
844
845    #[test]
846    fn test_workbook_insert_rows() {
847        let mut wb = Workbook::new();
848        wb.set_cell_value("Sheet1", "A1", "stay").unwrap();
849        wb.set_cell_value("Sheet1", "A2", "shift").unwrap();
850        wb.insert_rows("Sheet1", 2, 1).unwrap();
851
852        assert_eq!(
853            wb.get_cell_value("Sheet1", "A1").unwrap(),
854            CellValue::String("stay".to_string())
855        );
856        assert_eq!(
857            wb.get_cell_value("Sheet1", "A3").unwrap(),
858            CellValue::String("shift".to_string())
859        );
860        assert_eq!(wb.get_cell_value("Sheet1", "A2").unwrap(), CellValue::Empty);
861    }
862
863    #[test]
864    fn test_workbook_insert_rows_updates_formula_and_ranges() {
865        let mut wb = Workbook::new();
866        wb.set_cell_value(
867            "Sheet1",
868            "C1",
869            CellValue::Formula {
870                expr: "SUM(A2:B2)".to_string(),
871                result: None,
872            },
873        )
874        .unwrap();
875        wb.add_data_validation(
876            "Sheet1",
877            &crate::validation::DataValidationConfig::whole_number("A2:A5", 1, 9),
878        )
879        .unwrap();
880        wb.set_auto_filter("Sheet1", "A2:B10").unwrap();
881        wb.merge_cells("Sheet1", "A2", "B3").unwrap();
882
883        wb.insert_rows("Sheet1", 2, 1).unwrap();
884
885        match wb.get_cell_value("Sheet1", "C1").unwrap() {
886            CellValue::Formula { expr, .. } => assert_eq!(expr, "SUM(A3:B3)"),
887            other => panic!("expected formula, got {other:?}"),
888        }
889
890        let validations = wb.get_data_validations("Sheet1").unwrap();
891        assert_eq!(validations.len(), 1);
892        assert_eq!(validations[0].sqref, "A3:A6");
893
894        let merges = wb.get_merge_cells("Sheet1").unwrap();
895        assert_eq!(merges, vec!["A3:B4".to_string()]);
896
897        let ws = wb.worksheet_ref("Sheet1").unwrap();
898        assert_eq!(ws.auto_filter.as_ref().unwrap().reference, "A3:B11");
899    }
900
901    #[test]
902    fn test_workbook_insert_rows_sheet_not_found() {
903        let mut wb = Workbook::new();
904        let result = wb.insert_rows("NoSheet", 1, 1);
905        assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
906    }
907
908    #[test]
909    fn test_workbook_remove_row() {
910        let mut wb = Workbook::new();
911        wb.set_cell_value("Sheet1", "A1", "first").unwrap();
912        wb.set_cell_value("Sheet1", "A2", "second").unwrap();
913        wb.set_cell_value("Sheet1", "A3", "third").unwrap();
914        wb.remove_row("Sheet1", 2).unwrap();
915
916        assert_eq!(
917            wb.get_cell_value("Sheet1", "A1").unwrap(),
918            CellValue::String("first".to_string())
919        );
920        assert_eq!(
921            wb.get_cell_value("Sheet1", "A2").unwrap(),
922            CellValue::String("third".to_string())
923        );
924    }
925
926    #[test]
927    fn test_workbook_remove_row_sheet_not_found() {
928        let mut wb = Workbook::new();
929        let result = wb.remove_row("NoSheet", 1);
930        assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
931    }
932
933    #[test]
934    fn test_workbook_duplicate_row() {
935        let mut wb = Workbook::new();
936        wb.set_cell_value("Sheet1", "A1", "original").unwrap();
937        wb.duplicate_row("Sheet1", 1).unwrap();
938
939        assert_eq!(
940            wb.get_cell_value("Sheet1", "A1").unwrap(),
941            CellValue::String("original".to_string())
942        );
943        // The duplicated row at row 2 has the same SST index.
944        assert_eq!(
945            wb.get_cell_value("Sheet1", "A2").unwrap(),
946            CellValue::String("original".to_string())
947        );
948    }
949
950    #[test]
951    fn test_workbook_set_and_get_row_height() {
952        let mut wb = Workbook::new();
953        wb.set_row_height("Sheet1", 3, 25.0).unwrap();
954        assert_eq!(wb.get_row_height("Sheet1", 3).unwrap(), Some(25.0));
955    }
956
957    #[test]
958    fn test_workbook_get_row_height_sheet_not_found() {
959        let wb = Workbook::new();
960        let result = wb.get_row_height("NoSheet", 1);
961        assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
962    }
963
964    #[test]
965    fn test_workbook_set_row_visible() {
966        let mut wb = Workbook::new();
967        wb.set_row_visible("Sheet1", 1, false).unwrap();
968    }
969
970    #[test]
971    fn test_workbook_set_row_visible_sheet_not_found() {
972        let mut wb = Workbook::new();
973        let result = wb.set_row_visible("NoSheet", 1, false);
974        assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
975    }
976
977    #[test]
978    fn test_workbook_set_and_get_col_width() {
979        let mut wb = Workbook::new();
980        wb.set_col_width("Sheet1", "A", 18.0).unwrap();
981        assert_eq!(wb.get_col_width("Sheet1", "A").unwrap(), Some(18.0));
982    }
983
984    #[test]
985    fn test_workbook_get_col_width_sheet_not_found() {
986        let wb = Workbook::new();
987        let result = wb.get_col_width("NoSheet", "A");
988        assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
989    }
990
991    #[test]
992    fn test_workbook_set_col_visible() {
993        let mut wb = Workbook::new();
994        wb.set_col_visible("Sheet1", "B", false).unwrap();
995    }
996
997    #[test]
998    fn test_workbook_set_col_visible_sheet_not_found() {
999        let mut wb = Workbook::new();
1000        let result = wb.set_col_visible("NoSheet", "A", false);
1001        assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
1002    }
1003
1004    #[test]
1005    fn test_workbook_insert_cols() {
1006        let mut wb = Workbook::new();
1007        wb.set_cell_value("Sheet1", "A1", "a").unwrap();
1008        wb.set_cell_value("Sheet1", "B1", "b").unwrap();
1009        wb.insert_cols("Sheet1", "B", 1).unwrap();
1010
1011        assert_eq!(
1012            wb.get_cell_value("Sheet1", "A1").unwrap(),
1013            CellValue::String("a".to_string())
1014        );
1015        assert_eq!(wb.get_cell_value("Sheet1", "B1").unwrap(), CellValue::Empty);
1016        assert_eq!(
1017            wb.get_cell_value("Sheet1", "C1").unwrap(),
1018            CellValue::String("b".to_string())
1019        );
1020    }
1021
1022    #[test]
1023    fn test_workbook_insert_cols_updates_formula_and_ranges() {
1024        let mut wb = Workbook::new();
1025        wb.set_cell_value(
1026            "Sheet1",
1027            "D1",
1028            CellValue::Formula {
1029                expr: "SUM(A1:B1)".to_string(),
1030                result: None,
1031            },
1032        )
1033        .unwrap();
1034        wb.add_data_validation(
1035            "Sheet1",
1036            &crate::validation::DataValidationConfig::whole_number("B2:C3", 1, 9),
1037        )
1038        .unwrap();
1039        wb.set_auto_filter("Sheet1", "A1:C10").unwrap();
1040        wb.merge_cells("Sheet1", "B3", "C4").unwrap();
1041
1042        wb.insert_cols("Sheet1", "B", 2).unwrap();
1043
1044        match wb.get_cell_value("Sheet1", "F1").unwrap() {
1045            CellValue::Formula { expr, .. } => assert_eq!(expr, "SUM(A1:D1)"),
1046            other => panic!("expected formula, got {other:?}"),
1047        }
1048
1049        let validations = wb.get_data_validations("Sheet1").unwrap();
1050        assert_eq!(validations.len(), 1);
1051        assert_eq!(validations[0].sqref, "D2:E3");
1052
1053        let merges = wb.get_merge_cells("Sheet1").unwrap();
1054        assert_eq!(merges, vec!["D3:E4".to_string()]);
1055
1056        let ws = wb.worksheet_ref("Sheet1").unwrap();
1057        assert_eq!(ws.auto_filter.as_ref().unwrap().reference, "A1:E10");
1058    }
1059
1060    #[test]
1061    fn test_workbook_insert_cols_sheet_not_found() {
1062        let mut wb = Workbook::new();
1063        let result = wb.insert_cols("NoSheet", "A", 1);
1064        assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
1065    }
1066
1067    #[test]
1068    fn test_workbook_remove_col() {
1069        let mut wb = Workbook::new();
1070        wb.set_cell_value("Sheet1", "A1", "a").unwrap();
1071        wb.set_cell_value("Sheet1", "B1", "b").unwrap();
1072        wb.set_cell_value("Sheet1", "C1", "c").unwrap();
1073        wb.remove_col("Sheet1", "B").unwrap();
1074
1075        assert_eq!(
1076            wb.get_cell_value("Sheet1", "A1").unwrap(),
1077            CellValue::String("a".to_string())
1078        );
1079        assert_eq!(
1080            wb.get_cell_value("Sheet1", "B1").unwrap(),
1081            CellValue::String("c".to_string())
1082        );
1083    }
1084
1085    #[test]
1086    fn test_workbook_remove_col_sheet_not_found() {
1087        let mut wb = Workbook::new();
1088        let result = wb.remove_col("NoSheet", "A");
1089        assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
1090    }
1091
1092    #[test]
1093    fn test_new_stream_writer_validates_name() {
1094        let wb = Workbook::new();
1095        let result = wb.new_stream_writer("Bad[Name");
1096        assert!(result.is_err());
1097        assert!(matches!(result.unwrap_err(), Error::InvalidSheetName(_)));
1098    }
1099
1100    #[test]
1101    fn test_new_stream_writer_rejects_duplicate() {
1102        let wb = Workbook::new();
1103        let result = wb.new_stream_writer("Sheet1");
1104        assert!(result.is_err());
1105        assert!(matches!(
1106            result.unwrap_err(),
1107            Error::SheetAlreadyExists { .. }
1108        ));
1109    }
1110
1111    #[test]
1112    fn test_new_stream_writer_valid_name() {
1113        let wb = Workbook::new();
1114        let sw = wb.new_stream_writer("StreamSheet").unwrap();
1115        assert_eq!(sw.sheet_name(), "StreamSheet");
1116    }
1117
1118    #[test]
1119    fn test_apply_stream_writer_adds_sheet() {
1120        let mut wb = Workbook::new();
1121        let mut sw = wb.new_stream_writer("StreamSheet").unwrap();
1122        sw.write_row(1, &[CellValue::from("Hello"), CellValue::from(42)])
1123            .unwrap();
1124        let idx = wb.apply_stream_writer(sw).unwrap();
1125        assert_eq!(idx, 1);
1126        assert_eq!(wb.sheet_names(), vec!["Sheet1", "StreamSheet"]);
1127    }
1128
1129    #[test]
1130    fn test_apply_stream_writer_uses_inline_strings() {
1131        // Streamed sheets use inline strings, not the shared string table.
1132        let mut wb = Workbook::new();
1133        wb.set_cell_value("Sheet1", "A1", "Existing").unwrap();
1134        let sst_before = wb.sst_runtime.len();
1135
1136        let mut sw = wb.new_stream_writer("StreamSheet").unwrap();
1137        sw.write_row(1, &[CellValue::from("New"), CellValue::from("Existing")])
1138            .unwrap();
1139        wb.apply_stream_writer(sw).unwrap();
1140
1141        // SST should not grow because streamed sheets use inline strings.
1142        assert_eq!(wb.sst_runtime.len(), sst_before);
1143    }
1144
1145    #[test]
1146    fn test_stream_writer_save_and_reopen() {
1147        let dir = TempDir::new().unwrap();
1148        let path = dir.path().join("stream_test.xlsx");
1149
1150        let mut wb = Workbook::new();
1151        wb.set_cell_value("Sheet1", "A1", "Normal").unwrap();
1152
1153        let mut sw = wb.new_stream_writer("Streamed").unwrap();
1154        sw.write_row(1, &[CellValue::from("Name"), CellValue::from("Value")])
1155            .unwrap();
1156        sw.write_row(2, &[CellValue::from("Alice"), CellValue::from(100)])
1157            .unwrap();
1158        sw.write_row(3, &[CellValue::from("Bob"), CellValue::from(200)])
1159            .unwrap();
1160        wb.apply_stream_writer(sw).unwrap();
1161
1162        wb.save(&path).unwrap();
1163
1164        let wb2 = Workbook::open(&path).unwrap();
1165        assert_eq!(wb2.sheet_names(), vec!["Sheet1", "Streamed"]);
1166        assert_eq!(
1167            wb2.get_cell_value("Sheet1", "A1").unwrap(),
1168            CellValue::String("Normal".to_string())
1169        );
1170        assert_eq!(
1171            wb2.get_cell_value("Streamed", "A1").unwrap(),
1172            CellValue::String("Name".to_string())
1173        );
1174        assert_eq!(
1175            wb2.get_cell_value("Streamed", "B2").unwrap(),
1176            CellValue::Number(100.0)
1177        );
1178        assert_eq!(
1179            wb2.get_cell_value("Streamed", "A3").unwrap(),
1180            CellValue::String("Bob".to_string())
1181        );
1182    }
1183
1184    #[test]
1185    fn test_workbook_get_rows_empty_sheet() {
1186        let wb = Workbook::new();
1187        let rows = wb.get_rows("Sheet1").unwrap();
1188        assert!(rows.is_empty());
1189    }
1190
1191    #[test]
1192    fn test_workbook_get_rows_with_data() {
1193        let mut wb = Workbook::new();
1194        wb.set_cell_value("Sheet1", "A1", "Name").unwrap();
1195        wb.set_cell_value("Sheet1", "B1", 42.0).unwrap();
1196        wb.set_cell_value("Sheet1", "A2", "Alice").unwrap();
1197        wb.set_cell_value("Sheet1", "B2", true).unwrap();
1198
1199        let rows = wb.get_rows("Sheet1").unwrap();
1200        assert_eq!(rows.len(), 2);
1201        assert_eq!(rows[0].0, 1);
1202        assert_eq!(rows[0].1.len(), 2);
1203        assert_eq!(rows[0].1[0].0, 1);
1204        assert_eq!(rows[0].1[0].1, CellValue::String("Name".to_string()));
1205        assert_eq!(rows[0].1[1].0, 2);
1206        assert_eq!(rows[0].1[1].1, CellValue::Number(42.0));
1207        assert_eq!(rows[1].0, 2);
1208        assert_eq!(rows[1].1[0].1, CellValue::String("Alice".to_string()));
1209        assert_eq!(rows[1].1[1].1, CellValue::Bool(true));
1210    }
1211
1212    #[test]
1213    fn test_workbook_get_rows_sheet_not_found() {
1214        let wb = Workbook::new();
1215        assert!(wb.get_rows("NoSheet").is_err());
1216    }
1217
1218    #[test]
1219    fn test_workbook_get_cols_empty_sheet() {
1220        let wb = Workbook::new();
1221        let cols = wb.get_cols("Sheet1").unwrap();
1222        assert!(cols.is_empty());
1223    }
1224
1225    #[test]
1226    fn test_workbook_get_cols_with_data() {
1227        let mut wb = Workbook::new();
1228        wb.set_cell_value("Sheet1", "A1", "Name").unwrap();
1229        wb.set_cell_value("Sheet1", "B1", 42.0).unwrap();
1230        wb.set_cell_value("Sheet1", "A2", "Alice").unwrap();
1231        wb.set_cell_value("Sheet1", "B2", 30.0).unwrap();
1232
1233        let cols = wb.get_cols("Sheet1").unwrap();
1234        assert_eq!(cols.len(), 2);
1235        assert_eq!(cols[0].0, "A");
1236        assert_eq!(cols[0].1.len(), 2);
1237        assert_eq!(cols[0].1[0], (1, CellValue::String("Name".to_string())));
1238        assert_eq!(cols[0].1[1], (2, CellValue::String("Alice".to_string())));
1239        assert_eq!(cols[1].0, "B");
1240        assert_eq!(cols[1].1[0], (1, CellValue::Number(42.0)));
1241        assert_eq!(cols[1].1[1], (2, CellValue::Number(30.0)));
1242    }
1243
1244    #[test]
1245    fn test_workbook_get_cols_sheet_not_found() {
1246        let wb = Workbook::new();
1247        assert!(wb.get_cols("NoSheet").is_err());
1248    }
1249
1250    #[test]
1251    fn test_streamed_sheet_cells_empty_before_save() {
1252        // Streamed sheet data lives in a temp file, not in the WorksheetXml.
1253        // Reading cells before save returns Empty.
1254        let mut wb = Workbook::new();
1255        let mut sw = wb.new_stream_writer("Streamed").unwrap();
1256        sw.write_row(1, &[CellValue::from("Name"), CellValue::from("Age")])
1257            .unwrap();
1258        wb.apply_stream_writer(sw).unwrap();
1259
1260        assert_eq!(
1261            wb.get_cell_value("Streamed", "A1").unwrap(),
1262            CellValue::Empty
1263        );
1264        assert_eq!(
1265            wb.get_cell_value("Streamed", "B1").unwrap(),
1266            CellValue::Empty
1267        );
1268    }
1269
1270    #[test]
1271    fn test_streamed_sheet_readable_after_save_reopen() {
1272        let dir = TempDir::new().unwrap();
1273        let path = dir.path().join("stream_reopen.xlsx");
1274
1275        let mut wb = Workbook::new();
1276        let mut sw = wb.new_stream_writer("Streamed").unwrap();
1277        sw.write_row(1, &[CellValue::from("Name"), CellValue::from("Age")])
1278            .unwrap();
1279        sw.write_row(2, &[CellValue::from("Alice"), CellValue::from(30)])
1280            .unwrap();
1281        wb.apply_stream_writer(sw).unwrap();
1282        wb.save(&path).unwrap();
1283
1284        let wb2 = Workbook::open(&path).unwrap();
1285        assert_eq!(
1286            wb2.get_cell_value("Streamed", "A1").unwrap(),
1287            CellValue::String("Name".to_string())
1288        );
1289        assert_eq!(
1290            wb2.get_cell_value("Streamed", "B1").unwrap(),
1291            CellValue::String("Age".to_string())
1292        );
1293        assert_eq!(
1294            wb2.get_cell_value("Streamed", "A2").unwrap(),
1295            CellValue::String("Alice".to_string())
1296        );
1297        assert_eq!(
1298            wb2.get_cell_value("Streamed", "B2").unwrap(),
1299            CellValue::Number(30.0)
1300        );
1301    }
1302
1303    #[test]
1304    fn test_workbook_get_rows_roundtrip_save_open() {
1305        let mut wb = Workbook::new();
1306        wb.set_cell_value("Sheet1", "A1", "hello").unwrap();
1307        wb.set_cell_value("Sheet1", "B1", 99.0).unwrap();
1308        wb.set_cell_value("Sheet1", "A2", true).unwrap();
1309
1310        let tmp = std::env::temp_dir().join("test_get_rows_roundtrip.xlsx");
1311        wb.save(&tmp).unwrap();
1312
1313        let wb2 = Workbook::open(&tmp).unwrap();
1314        let rows = wb2.get_rows("Sheet1").unwrap();
1315        assert_eq!(rows.len(), 2);
1316        assert_eq!(rows[0].1[0].1, CellValue::String("hello".to_string()));
1317        assert_eq!(rows[0].1[1].1, CellValue::Number(99.0));
1318        assert_eq!(rows[1].1[0].1, CellValue::Bool(true));
1319
1320        let _ = std::fs::remove_file(&tmp);
1321    }
1322
1323    #[test]
1324    fn test_stream_save_reopen_basic() {
1325        let dir = TempDir::new().unwrap();
1326        let path = dir.path().join("stream_basic.xlsx");
1327
1328        let mut wb = Workbook::new();
1329        let mut sw = wb.new_stream_writer("Optimized").unwrap();
1330        sw.write_row(1, &[CellValue::from("Hello"), CellValue::from(42)])
1331            .unwrap();
1332        sw.write_row(2, &[CellValue::from("World"), CellValue::from(99)])
1333            .unwrap();
1334        let idx = wb.apply_stream_writer(sw).unwrap();
1335        assert_eq!(idx, 1);
1336
1337        wb.save(&path).unwrap();
1338        let wb2 = Workbook::open(&path).unwrap();
1339        assert_eq!(
1340            wb2.get_cell_value("Optimized", "A1").unwrap(),
1341            CellValue::String("Hello".to_string())
1342        );
1343        assert_eq!(
1344            wb2.get_cell_value("Optimized", "B1").unwrap(),
1345            CellValue::Number(42.0)
1346        );
1347        assert_eq!(
1348            wb2.get_cell_value("Optimized", "A2").unwrap(),
1349            CellValue::String("World".to_string())
1350        );
1351        assert_eq!(
1352            wb2.get_cell_value("Optimized", "B2").unwrap(),
1353            CellValue::Number(99.0)
1354        );
1355    }
1356
1357    #[test]
1358    fn test_stream_save_reopen_all_types() {
1359        let dir = TempDir::new().unwrap();
1360        let path = dir.path().join("stream_types.xlsx");
1361
1362        let mut wb = Workbook::new();
1363        let mut sw = wb.new_stream_writer("Types").unwrap();
1364        sw.write_row(
1365            1,
1366            &[
1367                CellValue::from("text"),
1368                CellValue::from(42),
1369                CellValue::from(3.14),
1370                CellValue::from(true),
1371                CellValue::Formula {
1372                    expr: "SUM(B1:C1)".to_string(),
1373                    result: None,
1374                },
1375                CellValue::Error("#N/A".to_string()),
1376                CellValue::Empty,
1377            ],
1378        )
1379        .unwrap();
1380        wb.apply_stream_writer(sw).unwrap();
1381
1382        wb.save(&path).unwrap();
1383        let wb2 = Workbook::open(&path).unwrap();
1384        assert_eq!(
1385            wb2.get_cell_value("Types", "A1").unwrap(),
1386            CellValue::String("text".to_string())
1387        );
1388        assert_eq!(
1389            wb2.get_cell_value("Types", "B1").unwrap(),
1390            CellValue::Number(42.0)
1391        );
1392        assert_eq!(
1393            wb2.get_cell_value("Types", "D1").unwrap(),
1394            CellValue::Bool(true)
1395        );
1396        match wb2.get_cell_value("Types", "E1").unwrap() {
1397            CellValue::Formula { expr, .. } => assert_eq!(expr, "SUM(B1:C1)"),
1398            other => panic!("expected formula, got {other:?}"),
1399        }
1400        assert_eq!(
1401            wb2.get_cell_value("Types", "F1").unwrap(),
1402            CellValue::Error("#N/A".to_string())
1403        );
1404        assert_eq!(wb2.get_cell_value("Types", "G1").unwrap(), CellValue::Empty);
1405    }
1406
1407    #[test]
1408    fn test_apply_stream_optimized_save_reopen() {
1409        let dir = TempDir::new().unwrap();
1410        let path = dir.path().join("stream_optimized.xlsx");
1411
1412        let mut wb = Workbook::new();
1413        wb.set_cell_value("Sheet1", "A1", "Normal").unwrap();
1414
1415        let mut sw = wb.new_stream_writer("Fast").unwrap();
1416        sw.write_row(1, &[CellValue::from("Name"), CellValue::from("Value")])
1417            .unwrap();
1418        sw.write_row(2, &[CellValue::from("Alice"), CellValue::from(100)])
1419            .unwrap();
1420        sw.write_row(3, &[CellValue::from("Bob"), CellValue::from(200)])
1421            .unwrap();
1422        wb.apply_stream_writer(sw).unwrap();
1423
1424        wb.save(&path).unwrap();
1425
1426        let wb2 = Workbook::open(&path).unwrap();
1427        assert_eq!(wb2.sheet_names(), vec!["Sheet1", "Fast"]);
1428        assert_eq!(
1429            wb2.get_cell_value("Fast", "A1").unwrap(),
1430            CellValue::String("Name".to_string())
1431        );
1432        assert_eq!(
1433            wb2.get_cell_value("Fast", "B2").unwrap(),
1434            CellValue::Number(100.0)
1435        );
1436        assert_eq!(
1437            wb2.get_cell_value("Fast", "A3").unwrap(),
1438            CellValue::String("Bob".to_string())
1439        );
1440    }
1441
1442    #[test]
1443    fn test_stream_freeze_panes_roundtrip() {
1444        let dir = TempDir::new().unwrap();
1445        let path = dir.path().join("stream_freeze.xlsx");
1446
1447        let mut wb = Workbook::new();
1448        let mut sw = wb.new_stream_writer("FreezeSheet").unwrap();
1449        sw.set_freeze_panes("B3").unwrap();
1450        sw.write_row(1, &[CellValue::from("A"), CellValue::from("B")])
1451            .unwrap();
1452        sw.write_row(2, &[CellValue::from("C"), CellValue::from("D")])
1453            .unwrap();
1454        wb.apply_stream_writer(sw).unwrap();
1455        wb.save(&path).unwrap();
1456
1457        let wb2 = Workbook::open(&path).unwrap();
1458        assert_eq!(
1459            wb2.get_panes("FreezeSheet").unwrap(),
1460            Some("B3".to_string())
1461        );
1462        assert_eq!(
1463            wb2.get_cell_value("FreezeSheet", "A1").unwrap(),
1464            CellValue::String("A".to_string())
1465        );
1466    }
1467
1468    #[test]
1469    fn test_stream_merge_cells_roundtrip() {
1470        let dir = TempDir::new().unwrap();
1471        let path = dir.path().join("stream_merge.xlsx");
1472
1473        let mut wb = Workbook::new();
1474        let mut sw = wb.new_stream_writer("MergeSheet").unwrap();
1475        sw.add_merge_cell("A1:C1").unwrap();
1476        sw.add_merge_cell("A3:B4").unwrap();
1477        sw.write_row(1, &[CellValue::from("Header")]).unwrap();
1478        sw.write_row(2, &[CellValue::from("Data")]).unwrap();
1479        wb.apply_stream_writer(sw).unwrap();
1480        wb.save(&path).unwrap();
1481
1482        let wb2 = Workbook::open(&path).unwrap();
1483        let merges = wb2.get_merge_cells("MergeSheet").unwrap();
1484        assert!(merges.contains(&"A1:C1".to_string()));
1485        assert!(merges.contains(&"A3:B4".to_string()));
1486        assert_eq!(
1487            wb2.get_cell_value("MergeSheet", "A1").unwrap(),
1488            CellValue::String("Header".to_string())
1489        );
1490    }
1491
1492    #[test]
1493    fn test_stream_col_widths_roundtrip() {
1494        let dir = TempDir::new().unwrap();
1495        let path = dir.path().join("stream_colw.xlsx");
1496
1497        let mut wb = Workbook::new();
1498        let mut sw = wb.new_stream_writer("ColSheet").unwrap();
1499        sw.set_col_width(1, 25.0).unwrap();
1500        sw.set_col_width(2, 12.5).unwrap();
1501        sw.write_row(1, &[CellValue::from("Wide"), CellValue::from("Narrow")])
1502            .unwrap();
1503        wb.apply_stream_writer(sw).unwrap();
1504        wb.save(&path).unwrap();
1505
1506        let wb2 = Workbook::open(&path).unwrap();
1507        let w1 = wb2.get_col_width("ColSheet", "A").unwrap().unwrap();
1508        let w2 = wb2.get_col_width("ColSheet", "B").unwrap().unwrap();
1509        assert!((w1 - 25.0).abs() < 0.01);
1510        assert!((w2 - 12.5).abs() < 0.01);
1511    }
1512
1513    #[test]
1514    fn test_stream_multiple_sheets() {
1515        let dir = TempDir::new().unwrap();
1516        let path = dir.path().join("stream_multi.xlsx");
1517
1518        let mut wb = Workbook::new();
1519        wb.set_cell_value("Sheet1", "A1", "Normal").unwrap();
1520
1521        let mut sw1 = wb.new_stream_writer("Stream1").unwrap();
1522        sw1.write_row(1, &[CellValue::from("S1R1")]).unwrap();
1523        sw1.write_row(2, &[CellValue::from("S1R2")]).unwrap();
1524        wb.apply_stream_writer(sw1).unwrap();
1525
1526        let mut sw2 = wb.new_stream_writer("Stream2").unwrap();
1527        sw2.write_row(1, &[CellValue::from("S2R1")]).unwrap();
1528        wb.apply_stream_writer(sw2).unwrap();
1529
1530        wb.save(&path).unwrap();
1531
1532        let wb2 = Workbook::open(&path).unwrap();
1533        assert_eq!(wb2.sheet_names(), vec!["Sheet1", "Stream1", "Stream2"]);
1534        assert_eq!(
1535            wb2.get_cell_value("Sheet1", "A1").unwrap(),
1536            CellValue::String("Normal".to_string())
1537        );
1538        assert_eq!(
1539            wb2.get_cell_value("Stream1", "A1").unwrap(),
1540            CellValue::String("S1R1".to_string())
1541        );
1542        assert_eq!(
1543            wb2.get_cell_value("Stream1", "A2").unwrap(),
1544            CellValue::String("S1R2".to_string())
1545        );
1546        assert_eq!(
1547            wb2.get_cell_value("Stream2", "A1").unwrap(),
1548            CellValue::String("S2R1".to_string())
1549        );
1550    }
1551
1552    #[test]
1553    fn test_stream_delete_sheet() {
1554        let dir = TempDir::new().unwrap();
1555        let path = dir.path().join("stream_delete.xlsx");
1556
1557        let mut wb = Workbook::new();
1558        let mut sw = wb.new_stream_writer("ToDelete").unwrap();
1559        sw.write_row(1, &[CellValue::from("Gone")]).unwrap();
1560        wb.apply_stream_writer(sw).unwrap();
1561
1562        let mut sw2 = wb.new_stream_writer("Kept").unwrap();
1563        sw2.write_row(1, &[CellValue::from("Stays")]).unwrap();
1564        wb.apply_stream_writer(sw2).unwrap();
1565
1566        wb.delete_sheet("ToDelete").unwrap();
1567        wb.save(&path).unwrap();
1568
1569        let wb2 = Workbook::open(&path).unwrap();
1570        assert_eq!(wb2.sheet_names(), vec!["Sheet1", "Kept"]);
1571        assert_eq!(
1572            wb2.get_cell_value("Kept", "A1").unwrap(),
1573            CellValue::String("Stays".to_string())
1574        );
1575    }
1576
1577    #[test]
1578    fn test_stream_combined_features_roundtrip() {
1579        let dir = TempDir::new().unwrap();
1580        let path = dir.path().join("stream_combined.xlsx");
1581
1582        let mut wb = Workbook::new();
1583        let mut sw = wb.new_stream_writer("Combined").unwrap();
1584        sw.set_freeze_panes("A2").unwrap();
1585        sw.set_col_width(1, 30.0).unwrap();
1586        sw.set_col_width_range(2, 3, 15.0).unwrap();
1587        sw.add_merge_cell("B1:C1").unwrap();
1588        sw.write_row(
1589            1,
1590            &[
1591                CellValue::from("Name"),
1592                CellValue::from("Merged Header"),
1593                CellValue::Empty,
1594            ],
1595        )
1596        .unwrap();
1597        sw.write_row(
1598            2,
1599            &[
1600                CellValue::from("Alice"),
1601                CellValue::from(100),
1602                CellValue::from(true),
1603            ],
1604        )
1605        .unwrap();
1606        wb.apply_stream_writer(sw).unwrap();
1607        wb.save(&path).unwrap();
1608
1609        let wb2 = Workbook::open(&path).unwrap();
1610        assert_eq!(wb2.get_panes("Combined").unwrap(), Some("A2".to_string()));
1611        let merges = wb2.get_merge_cells("Combined").unwrap();
1612        assert!(merges.contains(&"B1:C1".to_string()));
1613        let w1 = wb2.get_col_width("Combined", "A").unwrap().unwrap();
1614        assert!((w1 - 30.0).abs() < 0.01);
1615        assert_eq!(
1616            wb2.get_cell_value("Combined", "A1").unwrap(),
1617            CellValue::String("Name".to_string())
1618        );
1619        assert_eq!(
1620            wb2.get_cell_value("Combined", "B2").unwrap(),
1621            CellValue::Number(100.0)
1622        );
1623        assert_eq!(
1624            wb2.get_cell_value("Combined", "C2").unwrap(),
1625            CellValue::Bool(true)
1626        );
1627    }
1628
1629    // --- Regression tests for P1 bugs ---
1630
1631    #[test]
1632    fn test_stream_formula_result_types_roundtrip() {
1633        // Regression: formula cached results must preserve their type via the
1634        // cell t attribute (t="str", t="b", t="e"). Without it, string results
1635        // are dropped and bool results are decoded as Number(1.0).
1636        let dir = TempDir::new().unwrap();
1637        let path = dir.path().join("stream_formula_types.xlsx");
1638
1639        let mut wb = Workbook::new();
1640        let mut sw = wb.new_stream_writer("Formulas").unwrap();
1641        sw.write_row(
1642            1,
1643            &[
1644                CellValue::Formula {
1645                    expr: "A2&B2".to_string(),
1646                    result: Some(Box::new(CellValue::String("hello".to_string()))),
1647                },
1648                CellValue::Formula {
1649                    expr: "A2>0".to_string(),
1650                    result: Some(Box::new(CellValue::Bool(true))),
1651                },
1652                CellValue::Formula {
1653                    expr: "1/0".to_string(),
1654                    result: Some(Box::new(CellValue::Error("#DIV/0!".to_string()))),
1655                },
1656                CellValue::Formula {
1657                    expr: "SUM(A2:A10)".to_string(),
1658                    result: Some(Box::new(CellValue::Number(55.0))),
1659                },
1660            ],
1661        )
1662        .unwrap();
1663        wb.apply_stream_writer(sw).unwrap();
1664        wb.save(&path).unwrap();
1665
1666        let wb2 = Workbook::open(&path).unwrap();
1667        // String result
1668        assert_eq!(
1669            wb2.get_cell_value("Formulas", "A1").unwrap(),
1670            CellValue::Formula {
1671                expr: "A2&B2".to_string(),
1672                result: Some(Box::new(CellValue::String("hello".to_string()))),
1673            }
1674        );
1675        // Bool result
1676        assert_eq!(
1677            wb2.get_cell_value("Formulas", "B1").unwrap(),
1678            CellValue::Formula {
1679                expr: "A2>0".to_string(),
1680                result: Some(Box::new(CellValue::Bool(true))),
1681            }
1682        );
1683        // Error result
1684        assert_eq!(
1685            wb2.get_cell_value("Formulas", "C1").unwrap(),
1686            CellValue::Formula {
1687                expr: "1/0".to_string(),
1688                result: Some(Box::new(CellValue::Error("#DIV/0!".to_string()))),
1689            }
1690        );
1691        // Numeric result
1692        assert_eq!(
1693            wb2.get_cell_value("Formulas", "D1").unwrap(),
1694            CellValue::Formula {
1695                expr: "SUM(A2:A10)".to_string(),
1696                result: Some(Box::new(CellValue::Number(55.0))),
1697            }
1698        );
1699    }
1700
1701    #[test]
1702    fn test_stream_edit_after_apply_takes_effect() {
1703        // Regression: edits via set_cell_value after apply_stream_writer must
1704        // not be silently ignored. The edit invalidates the streamed data so
1705        // the normal WorksheetXml serialization path is used on save.
1706        let dir = TempDir::new().unwrap();
1707        let path = dir.path().join("stream_edit_after.xlsx");
1708
1709        let mut wb = Workbook::new();
1710        let mut sw = wb.new_stream_writer("S").unwrap();
1711        sw.write_row(1, &[CellValue::from("old")]).unwrap();
1712        wb.apply_stream_writer(sw).unwrap();
1713
1714        // Edit the streamed sheet: this should invalidate streamed data.
1715        wb.set_cell_value("S", "A1", "new").unwrap();
1716        wb.save(&path).unwrap();
1717
1718        let wb2 = Workbook::open(&path).unwrap();
1719        assert_eq!(
1720            wb2.get_cell_value("S", "A1").unwrap(),
1721            CellValue::String("new".to_string())
1722        );
1723    }
1724
1725    #[test]
1726    fn test_stream_copy_sheet_preserves_data() {
1727        // Regression: copy_sheet must clone the streamed payload so both
1728        // source and target sheets have the streamed data on save.
1729        let dir = TempDir::new().unwrap();
1730        let path = dir.path().join("stream_copy.xlsx");
1731
1732        let mut wb = Workbook::new();
1733        let mut sw = wb.new_stream_writer("Src").unwrap();
1734        sw.write_row(1, &[CellValue::from("x")]).unwrap();
1735        sw.write_row(2, &[CellValue::from("y")]).unwrap();
1736        wb.apply_stream_writer(sw).unwrap();
1737
1738        wb.copy_sheet("Src", "Dst").unwrap();
1739        wb.save(&path).unwrap();
1740
1741        let wb2 = Workbook::open(&path).unwrap();
1742        assert_eq!(
1743            wb2.get_cell_value("Src", "A1").unwrap(),
1744            CellValue::String("x".to_string())
1745        );
1746        assert_eq!(
1747            wb2.get_cell_value("Src", "A2").unwrap(),
1748            CellValue::String("y".to_string())
1749        );
1750        assert_eq!(
1751            wb2.get_cell_value("Dst", "A1").unwrap(),
1752            CellValue::String("x".to_string())
1753        );
1754        assert_eq!(
1755            wb2.get_cell_value("Dst", "A2").unwrap(),
1756            CellValue::String("y".to_string())
1757        );
1758    }
1759}