Skip to main content

jugar_probar/
file_ops.rs

1//! File Upload and Download Operations (Feature G.8)
2//!
3//! Provides support for file input/output operations in E2E tests.
4
5use serde::{Deserialize, Serialize};
6use std::path::{Path, PathBuf};
7
8/// File input for upload operations
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct FileInput {
11    /// File name
12    pub name: String,
13    /// MIME type
14    pub mime_type: String,
15    /// File contents
16    pub contents: Vec<u8>,
17    /// Original path (if from filesystem)
18    pub path: Option<PathBuf>,
19}
20
21impl FileInput {
22    /// Create a new file input
23    #[must_use]
24    pub fn new(name: impl Into<String>, mime_type: impl Into<String>, contents: Vec<u8>) -> Self {
25        Self {
26            name: name.into(),
27            mime_type: mime_type.into(),
28            contents,
29            path: None,
30        }
31    }
32
33    /// Create from path (mock - doesn't actually read file)
34    #[must_use]
35    pub fn from_path(path: impl AsRef<Path>) -> Self {
36        let path = path.as_ref();
37        let name = path
38            .file_name()
39            .map(|n| n.to_string_lossy().to_string())
40            .unwrap_or_else(|| "unknown".to_string());
41
42        let mime_type = guess_mime_type(&name);
43
44        Self {
45            name,
46            mime_type,
47            contents: Vec::new(), // Empty in mock
48            path: Some(path.to_path_buf()),
49        }
50    }
51
52    /// Create from path with contents
53    #[must_use]
54    pub fn from_path_with_contents(path: impl AsRef<Path>, contents: Vec<u8>) -> Self {
55        let path = path.as_ref();
56        let name = path
57            .file_name()
58            .map(|n| n.to_string_lossy().to_string())
59            .unwrap_or_else(|| "unknown".to_string());
60
61        let mime_type = guess_mime_type(&name);
62
63        Self {
64            name,
65            mime_type,
66            contents,
67            path: Some(path.to_path_buf()),
68        }
69    }
70
71    /// Create a text file
72    #[must_use]
73    pub fn text(name: impl Into<String>, content: impl Into<String>) -> Self {
74        Self::new(name, "text/plain", content.into().into_bytes())
75    }
76
77    /// Create a JSON file
78    #[must_use]
79    pub fn json(name: impl Into<String>, content: impl Into<String>) -> Self {
80        Self::new(name, "application/json", content.into().into_bytes())
81    }
82
83    /// Create a CSV file
84    #[must_use]
85    pub fn csv(name: impl Into<String>, content: impl Into<String>) -> Self {
86        Self::new(name, "text/csv", content.into().into_bytes())
87    }
88
89    /// Create a PNG image (mock)
90    #[must_use]
91    pub fn png(name: impl Into<String>, contents: Vec<u8>) -> Self {
92        Self::new(name, "image/png", contents)
93    }
94
95    /// Create a PDF document (mock)
96    #[must_use]
97    pub fn pdf(name: impl Into<String>, contents: Vec<u8>) -> Self {
98        Self::new(name, "application/pdf", contents)
99    }
100
101    /// Get file name
102    #[must_use]
103    pub fn name(&self) -> &str {
104        &self.name
105    }
106
107    /// Get MIME type
108    #[must_use]
109    pub fn mime_type(&self) -> &str {
110        &self.mime_type
111    }
112
113    /// Get file size
114    #[must_use]
115    pub fn size(&self) -> usize {
116        self.contents.len()
117    }
118
119    /// Get contents as bytes
120    #[must_use]
121    pub fn contents(&self) -> &[u8] {
122        &self.contents
123    }
124
125    /// Get contents as string (if valid UTF-8)
126    #[must_use]
127    pub fn contents_string(&self) -> Option<String> {
128        String::from_utf8(self.contents.clone()).ok()
129    }
130
131    /// Check if file is empty
132    #[must_use]
133    pub fn is_empty(&self) -> bool {
134        self.contents.is_empty()
135    }
136}
137
138/// Represents a file download
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct Download {
141    /// Suggested filename
142    pub suggested_filename: String,
143    /// URL the download came from
144    pub url: String,
145    /// File contents
146    pub contents: Vec<u8>,
147    /// Path where file was saved (if saved)
148    pub saved_path: Option<PathBuf>,
149    /// Download state
150    pub state: DownloadState,
151}
152
153impl Download {
154    /// Create a new download
155    #[must_use]
156    pub fn new(url: impl Into<String>, filename: impl Into<String>) -> Self {
157        Self {
158            suggested_filename: filename.into(),
159            url: url.into(),
160            contents: Vec::new(),
161            saved_path: None,
162            state: DownloadState::InProgress,
163        }
164    }
165
166    /// Create a completed download with contents
167    #[must_use]
168    pub fn completed(
169        url: impl Into<String>,
170        filename: impl Into<String>,
171        contents: Vec<u8>,
172    ) -> Self {
173        Self {
174            suggested_filename: filename.into(),
175            url: url.into(),
176            contents,
177            saved_path: None,
178            state: DownloadState::Completed,
179        }
180    }
181
182    /// Get suggested filename
183    #[must_use]
184    pub fn suggested_filename(&self) -> &str {
185        &self.suggested_filename
186    }
187
188    /// Get download URL
189    #[must_use]
190    pub fn url(&self) -> &str {
191        &self.url
192    }
193
194    /// Get file size
195    #[must_use]
196    pub fn size(&self) -> usize {
197        self.contents.len()
198    }
199
200    /// Check if download is complete
201    #[must_use]
202    pub fn is_complete(&self) -> bool {
203        matches!(self.state, DownloadState::Completed)
204    }
205
206    /// Check if download failed
207    #[must_use]
208    pub fn is_failed(&self) -> bool {
209        matches!(self.state, DownloadState::Failed(_))
210    }
211
212    /// Get path where file was saved
213    #[must_use]
214    pub fn path(&self) -> Option<&Path> {
215        self.saved_path.as_deref()
216    }
217
218    /// Mark as saved to path (mock)
219    pub fn save_as(&mut self, path: impl AsRef<Path>) {
220        self.saved_path = Some(path.as_ref().to_path_buf());
221        self.state = DownloadState::Completed;
222    }
223
224    /// Cancel the download
225    pub fn cancel(&mut self) {
226        self.state = DownloadState::Cancelled;
227    }
228
229    /// Mark as failed
230    pub fn fail(&mut self, reason: impl Into<String>) {
231        self.state = DownloadState::Failed(reason.into());
232    }
233
234    /// Get contents
235    #[must_use]
236    pub fn contents(&self) -> &[u8] {
237        &self.contents
238    }
239
240    /// Delete the downloaded file (mock)
241    pub fn delete(&mut self) {
242        self.saved_path = None;
243        self.state = DownloadState::Deleted;
244    }
245}
246
247/// State of a download
248#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
249pub enum DownloadState {
250    /// Download in progress
251    InProgress,
252    /// Download completed successfully
253    Completed,
254    /// Download was cancelled
255    Cancelled,
256    /// Download failed
257    Failed(String),
258    /// Downloaded file was deleted
259    Deleted,
260}
261
262/// File chooser for handling file input elements
263#[derive(Debug, Clone)]
264pub struct FileChooser {
265    /// Whether multiple files can be selected
266    pub multiple: bool,
267    /// Accepted file types (MIME types or extensions)
268    pub accept: Vec<String>,
269    /// Selected files
270    pub files: Vec<FileInput>,
271}
272
273impl FileChooser {
274    /// Create a new file chooser
275    #[must_use]
276    pub fn new() -> Self {
277        Self {
278            multiple: false,
279            accept: Vec::new(),
280            files: Vec::new(),
281        }
282    }
283
284    /// Create for single file selection
285    #[must_use]
286    pub fn single() -> Self {
287        Self::new()
288    }
289
290    /// Create for multiple file selection
291    #[must_use]
292    pub fn multiple() -> Self {
293        Self {
294            multiple: true,
295            ..Self::new()
296        }
297    }
298
299    /// Set accepted file types
300    #[must_use]
301    pub fn accept(mut self, types: impl IntoIterator<Item = impl Into<String>>) -> Self {
302        self.accept = types.into_iter().map(Into::into).collect();
303        self
304    }
305
306    /// Set a single file
307    pub fn set_files(&mut self, files: impl IntoIterator<Item = FileInput>) {
308        let files: Vec<FileInput> = files.into_iter().collect();
309        if !self.multiple && files.len() > 1 {
310            // Safe: we checked files.len() > 1, so there's at least one item
311            if let Some(first) = files.into_iter().next() {
312                self.files = vec![first];
313            }
314        } else {
315            self.files = files;
316        }
317    }
318
319    /// Set files from paths
320    pub fn set_input_files(&mut self, paths: &[impl AsRef<Path>]) {
321        let files: Vec<FileInput> = paths.iter().map(FileInput::from_path).collect();
322        self.set_files(files);
323    }
324
325    /// Check if file type is accepted
326    #[must_use]
327    pub fn is_accepted(&self, file: &FileInput) -> bool {
328        if self.accept.is_empty() {
329            return true;
330        }
331
332        let ext = file
333            .name
334            .rsplit('.')
335            .next()
336            .map(|e| format!(".{}", e.to_lowercase()));
337
338        for accept in &self.accept {
339            if accept == &file.mime_type {
340                return true;
341            }
342            if let Some(ref extension) = ext {
343                if accept == extension {
344                    return true;
345                }
346            }
347            if accept == "*/*" {
348                return true;
349            }
350            // Check MIME type patterns like "image/*"
351            if accept.ends_with("/*") {
352                let prefix = &accept[..accept.len() - 1];
353                if file.mime_type.starts_with(prefix) {
354                    return true;
355                }
356            }
357        }
358
359        false
360    }
361
362    /// Get selected files
363    #[must_use]
364    pub fn files(&self) -> &[FileInput] {
365        &self.files
366    }
367
368    /// Get file count
369    #[must_use]
370    pub fn file_count(&self) -> usize {
371        self.files.len()
372    }
373
374    /// Check if any files selected
375    #[must_use]
376    pub fn has_files(&self) -> bool {
377        !self.files.is_empty()
378    }
379
380    /// Clear selected files
381    pub fn clear(&mut self) {
382        self.files.clear();
383    }
384}
385
386impl Default for FileChooser {
387    fn default() -> Self {
388        Self::new()
389    }
390}
391
392/// Download manager for tracking downloads
393#[derive(Debug, Clone, Default)]
394pub struct DownloadManager {
395    downloads: Vec<Download>,
396}
397
398impl DownloadManager {
399    /// Create a new download manager
400    #[must_use]
401    pub fn new() -> Self {
402        Self::default()
403    }
404
405    /// Add a download
406    pub fn add(&mut self, download: Download) {
407        self.downloads.push(download);
408    }
409
410    /// Get all downloads
411    #[must_use]
412    pub fn downloads(&self) -> &[Download] {
413        &self.downloads
414    }
415
416    /// Get download count
417    #[must_use]
418    pub fn count(&self) -> usize {
419        self.downloads.len()
420    }
421
422    /// Get last download
423    #[must_use]
424    pub fn last(&self) -> Option<&Download> {
425        self.downloads.last()
426    }
427
428    /// Get mutable reference to last download
429    pub fn last_mut(&mut self) -> Option<&mut Download> {
430        self.downloads.last_mut()
431    }
432
433    /// Find download by filename
434    #[must_use]
435    pub fn find_by_name(&self, name: &str) -> Option<&Download> {
436        self.downloads.iter().find(|d| d.suggested_filename == name)
437    }
438
439    /// Clear all downloads
440    pub fn clear(&mut self) {
441        self.downloads.clear();
442    }
443
444    /// Get completed downloads
445    #[must_use]
446    pub fn completed(&self) -> Vec<&Download> {
447        self.downloads.iter().filter(|d| d.is_complete()).collect()
448    }
449
450    /// Wait for download (mock - returns last download)
451    #[must_use]
452    pub fn wait_for_download(&self) -> Option<&Download> {
453        self.last()
454    }
455}
456
457/// Guess MIME type from filename
458#[must_use]
459pub fn guess_mime_type(filename: &str) -> String {
460    let ext = filename
461        .rsplit('.')
462        .next()
463        .map(str::to_lowercase)
464        .unwrap_or_default();
465
466    match ext.as_str() {
467        "txt" => "text/plain",
468        "html" | "htm" => "text/html",
469        "css" => "text/css",
470        "js" => "application/javascript",
471        "json" => "application/json",
472        "xml" => "application/xml",
473        "csv" => "text/csv",
474        "pdf" => "application/pdf",
475        "png" => "image/png",
476        "jpg" | "jpeg" => "image/jpeg",
477        "gif" => "image/gif",
478        "svg" => "image/svg+xml",
479        "webp" => "image/webp",
480        "ico" => "image/x-icon",
481        "mp3" => "audio/mpeg",
482        "wav" => "audio/wav",
483        "mp4" => "video/mp4",
484        "webm" => "video/webm",
485        "wasm" => "application/wasm",
486        "zip" => "application/zip",
487        "gz" => "application/gzip",
488        "tar" => "application/x-tar",
489        "doc" => "application/msword",
490        "docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
491        "xls" => "application/vnd.ms-excel",
492        "xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
493        _ => "application/octet-stream",
494    }
495    .to_string()
496}
497
498#[cfg(test)]
499#[allow(clippy::unwrap_used, clippy::expect_used)]
500mod tests {
501    use super::*;
502
503    // =========================================================================
504    // H₀-FILE-01: FileInput creation
505    // =========================================================================
506
507    #[test]
508    fn h0_file_01_new() {
509        let file = FileInput::new("test.txt", "text/plain", b"hello".to_vec());
510        assert_eq!(file.name(), "test.txt");
511        assert_eq!(file.mime_type(), "text/plain");
512        assert_eq!(file.contents(), b"hello");
513    }
514
515    #[test]
516    fn h0_file_02_from_path() {
517        let file = FileInput::from_path("documents/report.pdf");
518        assert_eq!(file.name(), "report.pdf");
519        assert_eq!(file.mime_type(), "application/pdf");
520    }
521
522    #[test]
523    fn h0_file_03_text() {
524        let file = FileInput::text("notes.txt", "Hello world");
525        assert_eq!(file.mime_type(), "text/plain");
526        assert_eq!(file.contents_string(), Some("Hello world".to_string()));
527    }
528
529    #[test]
530    fn h0_file_04_json() {
531        let file = FileInput::json("data.json", r#"{"key": "value"}"#);
532        assert_eq!(file.mime_type(), "application/json");
533    }
534
535    #[test]
536    fn h0_file_05_csv() {
537        let file = FileInput::csv("data.csv", "a,b,c\n1,2,3");
538        assert_eq!(file.mime_type(), "text/csv");
539    }
540
541    #[test]
542    fn h0_file_06_png() {
543        let file = FileInput::png("image.png", vec![0x89, 0x50, 0x4E, 0x47]);
544        assert_eq!(file.mime_type(), "image/png");
545    }
546
547    #[test]
548    fn h0_file_07_pdf() {
549        let file = FileInput::pdf("doc.pdf", vec![0x25, 0x50, 0x44, 0x46]);
550        assert_eq!(file.mime_type(), "application/pdf");
551    }
552
553    // =========================================================================
554    // H₀-FILE-08: FileInput properties
555    // =========================================================================
556
557    #[test]
558    fn h0_file_08_size() {
559        let file = FileInput::text("test.txt", "12345");
560        assert_eq!(file.size(), 5);
561    }
562
563    #[test]
564    fn h0_file_09_is_empty() {
565        let empty = FileInput::new("empty.txt", "text/plain", vec![]);
566        assert!(empty.is_empty());
567
568        let non_empty = FileInput::text("full.txt", "content");
569        assert!(!non_empty.is_empty());
570    }
571
572    #[test]
573    fn h0_file_10_contents_string_valid() {
574        let file = FileInput::text("test.txt", "Hello");
575        assert_eq!(file.contents_string(), Some("Hello".to_string()));
576    }
577
578    #[test]
579    fn h0_file_11_contents_string_invalid() {
580        let file = FileInput::new("binary.bin", "application/octet-stream", vec![0xFF, 0xFE]);
581        assert!(file.contents_string().is_none());
582    }
583
584    // =========================================================================
585    // H₀-FILE-12: Download creation
586    // =========================================================================
587
588    #[test]
589    fn h0_file_12_download_new() {
590        let download = Download::new("http://example.com/file.pdf", "file.pdf");
591        assert_eq!(download.suggested_filename(), "file.pdf");
592        assert_eq!(download.url(), "http://example.com/file.pdf");
593        assert!(!download.is_complete());
594    }
595
596    #[test]
597    fn h0_file_13_download_completed() {
598        let download =
599            Download::completed("http://example.com/data.json", "data.json", b"{}".to_vec());
600        assert!(download.is_complete());
601        assert_eq!(download.size(), 2);
602    }
603
604    // =========================================================================
605    // H₀-FILE-14: Download operations
606    // =========================================================================
607
608    #[test]
609    fn h0_file_14_save_as() {
610        let mut download = Download::completed("http://test", "file.txt", b"content".to_vec());
611        download.save_as("/tmp/file.txt");
612
613        assert_eq!(download.path(), Some(Path::new("/tmp/file.txt")));
614    }
615
616    #[test]
617    fn h0_file_15_cancel() {
618        let mut download = Download::new("http://test", "file.txt");
619        download.cancel();
620
621        assert_eq!(download.state, DownloadState::Cancelled);
622    }
623
624    #[test]
625    fn h0_file_16_fail() {
626        let mut download = Download::new("http://test", "file.txt");
627        download.fail("Network error");
628
629        assert!(download.is_failed());
630        assert_eq!(
631            download.state,
632            DownloadState::Failed("Network error".to_string())
633        );
634    }
635
636    #[test]
637    fn h0_file_17_delete() {
638        let mut download = Download::completed("http://test", "file.txt", vec![]);
639        download.save_as("/tmp/file.txt");
640        download.delete();
641
642        assert!(download.path().is_none());
643        assert_eq!(download.state, DownloadState::Deleted);
644    }
645
646    // =========================================================================
647    // H₀-FILE-18: FileChooser
648    // =========================================================================
649
650    #[test]
651    fn h0_file_18_chooser_new() {
652        let chooser = FileChooser::new();
653        assert!(!chooser.multiple);
654        assert!(!chooser.has_files());
655    }
656
657    #[test]
658    fn h0_file_19_chooser_multiple() {
659        let chooser = FileChooser::multiple();
660        assert!(chooser.multiple);
661    }
662
663    #[test]
664    fn h0_file_20_chooser_accept() {
665        let chooser = FileChooser::new().accept(vec![".pdf", ".doc"]);
666        assert_eq!(chooser.accept.len(), 2);
667    }
668
669    #[test]
670    fn h0_file_21_set_files() {
671        let mut chooser = FileChooser::new();
672        chooser.set_files(vec![FileInput::text("test.txt", "content")]);
673
674        assert_eq!(chooser.file_count(), 1);
675    }
676
677    #[test]
678    fn h0_file_22_set_files_single_mode() {
679        let mut chooser = FileChooser::single();
680        chooser.set_files(vec![
681            FileInput::text("a.txt", "a"),
682            FileInput::text("b.txt", "b"),
683        ]);
684
685        // Should only keep first file
686        assert_eq!(chooser.file_count(), 1);
687        assert_eq!(chooser.files()[0].name(), "a.txt");
688    }
689
690    #[test]
691    fn h0_file_23_set_files_multiple_mode() {
692        let mut chooser = FileChooser::multiple();
693        chooser.set_files(vec![
694            FileInput::text("a.txt", "a"),
695            FileInput::text("b.txt", "b"),
696        ]);
697
698        assert_eq!(chooser.file_count(), 2);
699    }
700
701    // =========================================================================
702    // H₀-FILE-24: FileChooser accept validation
703    // =========================================================================
704
705    #[test]
706    fn h0_file_24_is_accepted_empty() {
707        let chooser = FileChooser::new();
708        let file = FileInput::text("test.txt", "");
709        assert!(chooser.is_accepted(&file));
710    }
711
712    #[test]
713    fn h0_file_25_is_accepted_mime() {
714        let chooser = FileChooser::new().accept(vec!["text/plain"]);
715        let file = FileInput::text("test.txt", "");
716        assert!(chooser.is_accepted(&file));
717    }
718
719    #[test]
720    fn h0_file_26_is_accepted_extension() {
721        let chooser = FileChooser::new().accept(vec![".pdf"]);
722        let file = FileInput::from_path("doc.pdf");
723        assert!(chooser.is_accepted(&file));
724    }
725
726    #[test]
727    fn h0_file_27_is_accepted_wildcard() {
728        let chooser = FileChooser::new().accept(vec!["image/*"]);
729        let png = FileInput::png("test.png", vec![]);
730        assert!(chooser.is_accepted(&png));
731    }
732
733    #[test]
734    fn h0_file_28_is_not_accepted() {
735        let chooser = FileChooser::new().accept(vec![".pdf"]);
736        let file = FileInput::text("test.txt", "");
737        assert!(!chooser.is_accepted(&file));
738    }
739
740    // =========================================================================
741    // H₀-FILE-29: DownloadManager
742    // =========================================================================
743
744    #[test]
745    fn h0_file_29_manager_new() {
746        let manager = DownloadManager::new();
747        assert_eq!(manager.count(), 0);
748    }
749
750    #[test]
751    fn h0_file_30_manager_add() {
752        let mut manager = DownloadManager::new();
753        manager.add(Download::new("http://test", "file.txt"));
754
755        assert_eq!(manager.count(), 1);
756    }
757
758    #[test]
759    fn h0_file_31_manager_last() {
760        let mut manager = DownloadManager::new();
761        manager.add(Download::new("http://test", "first.txt"));
762        manager.add(Download::new("http://test", "last.txt"));
763
764        let last = manager.last().unwrap();
765        assert_eq!(last.suggested_filename(), "last.txt");
766    }
767
768    #[test]
769    fn h0_file_32_manager_find_by_name() {
770        let mut manager = DownloadManager::new();
771        manager.add(Download::new("http://test/a", "a.txt"));
772        manager.add(Download::new("http://test/b", "b.txt"));
773
774        let found = manager.find_by_name("a.txt").unwrap();
775        assert_eq!(found.url(), "http://test/a");
776    }
777
778    #[test]
779    fn h0_file_33_manager_clear() {
780        let mut manager = DownloadManager::new();
781        manager.add(Download::new("http://test", "file.txt"));
782        manager.clear();
783
784        assert_eq!(manager.count(), 0);
785    }
786
787    #[test]
788    fn h0_file_34_manager_completed() {
789        let mut manager = DownloadManager::new();
790        manager.add(Download::completed("http://test/a", "a.txt", vec![]));
791        manager.add(Download::new("http://test/b", "b.txt"));
792
793        let completed = manager.completed();
794        assert_eq!(completed.len(), 1);
795    }
796
797    // =========================================================================
798    // H₀-FILE-35: MIME type guessing
799    // =========================================================================
800
801    #[test]
802    fn h0_file_35_guess_mime_text() {
803        assert_eq!(guess_mime_type("file.txt"), "text/plain");
804        assert_eq!(guess_mime_type("page.html"), "text/html");
805        assert_eq!(guess_mime_type("styles.css"), "text/css");
806    }
807
808    #[test]
809    fn h0_file_36_guess_mime_image() {
810        assert_eq!(guess_mime_type("photo.png"), "image/png");
811        assert_eq!(guess_mime_type("photo.jpg"), "image/jpeg");
812        assert_eq!(guess_mime_type("photo.jpeg"), "image/jpeg");
813        assert_eq!(guess_mime_type("icon.svg"), "image/svg+xml");
814    }
815
816    #[test]
817    fn h0_file_37_guess_mime_app() {
818        assert_eq!(guess_mime_type("data.json"), "application/json");
819        assert_eq!(guess_mime_type("doc.pdf"), "application/pdf");
820        assert_eq!(guess_mime_type("app.wasm"), "application/wasm");
821    }
822
823    #[test]
824    fn h0_file_38_guess_mime_unknown() {
825        assert_eq!(guess_mime_type("file.xyz"), "application/octet-stream");
826        assert_eq!(guess_mime_type("noextension"), "application/octet-stream");
827    }
828
829    // =========================================================================
830    // H₀-FILE-39: Clone and Debug
831    // =========================================================================
832
833    #[test]
834    fn h0_file_39_file_input_clone() {
835        let file = FileInput::text("test.txt", "content");
836        let cloned = file;
837        assert_eq!(cloned.name(), "test.txt");
838    }
839
840    #[test]
841    fn h0_file_40_download_clone() {
842        let download = Download::completed("http://test", "file.txt", vec![1, 2, 3]);
843        let cloned = download;
844        assert_eq!(cloned.size(), 3);
845    }
846
847    // =========================================================================
848    // H₀-FILE-41+: Additional coverage tests
849    // =========================================================================
850
851    #[test]
852    fn h0_file_41_from_path_with_contents() {
853        let file = FileInput::from_path_with_contents("docs/report.pdf", b"PDF content".to_vec());
854        assert_eq!(file.name(), "report.pdf");
855        assert_eq!(file.mime_type(), "application/pdf");
856        assert_eq!(file.contents(), b"PDF content");
857        assert!(file.path.is_some());
858        assert_eq!(file.path.unwrap().to_str().unwrap(), "docs/report.pdf");
859    }
860
861    #[test]
862    fn h0_file_42_from_path_no_filename() {
863        // Test path with no file name (edge case)
864        let file = FileInput::from_path("/");
865        assert_eq!(file.name(), "unknown");
866    }
867
868    #[test]
869    fn h0_file_43_from_path_with_contents_no_filename() {
870        // Test path with no file name (edge case)
871        let file = FileInput::from_path_with_contents("/", vec![1, 2, 3]);
872        assert_eq!(file.name(), "unknown");
873        assert_eq!(file.contents(), &[1, 2, 3]);
874    }
875
876    #[test]
877    fn h0_file_44_download_contents_accessor() {
878        let download = Download::completed("http://test", "file.txt", vec![1, 2, 3, 4, 5]);
879        assert_eq!(download.contents(), &[1, 2, 3, 4, 5]);
880    }
881
882    #[test]
883    fn h0_file_45_file_chooser_set_input_files() {
884        let mut chooser = FileChooser::multiple();
885        chooser.set_input_files(&["file1.txt", "file2.pdf"]);
886        assert_eq!(chooser.file_count(), 2);
887        assert_eq!(chooser.files()[0].name(), "file1.txt");
888        assert_eq!(chooser.files()[1].name(), "file2.pdf");
889    }
890
891    #[test]
892    fn h0_file_46_file_chooser_clear() {
893        let mut chooser = FileChooser::new();
894        chooser.set_files(vec![FileInput::text("test.txt", "content")]);
895        assert!(chooser.has_files());
896        chooser.clear();
897        assert!(!chooser.has_files());
898        assert_eq!(chooser.file_count(), 0);
899    }
900
901    #[test]
902    fn h0_file_47_file_chooser_default() {
903        let chooser = FileChooser::default();
904        assert!(!chooser.multiple);
905        assert!(chooser.accept.is_empty());
906        assert!(chooser.files.is_empty());
907    }
908
909    #[test]
910    fn h0_file_48_download_manager_last_mut() {
911        let mut manager = DownloadManager::new();
912        manager.add(Download::new("http://test", "file.txt"));
913
914        // Modify via last_mut
915        if let Some(download) = manager.last_mut() {
916            download.cancel();
917        }
918
919        assert_eq!(manager.last().unwrap().state, DownloadState::Cancelled);
920    }
921
922    #[test]
923    fn h0_file_49_download_manager_downloads_accessor() {
924        let mut manager = DownloadManager::new();
925        manager.add(Download::new("http://test/a", "a.txt"));
926        manager.add(Download::new("http://test/b", "b.txt"));
927
928        let downloads = manager.downloads();
929        assert_eq!(downloads.len(), 2);
930        assert_eq!(downloads[0].suggested_filename(), "a.txt");
931        assert_eq!(downloads[1].suggested_filename(), "b.txt");
932    }
933
934    #[test]
935    fn h0_file_50_download_manager_wait_for_download() {
936        let mut manager = DownloadManager::new();
937        assert!(manager.wait_for_download().is_none());
938
939        manager.add(Download::new("http://test", "file.txt"));
940        let waited = manager.wait_for_download();
941        assert!(waited.is_some());
942        assert_eq!(waited.unwrap().suggested_filename(), "file.txt");
943    }
944
945    #[test]
946    fn h0_file_51_download_manager_find_by_name_not_found() {
947        let manager = DownloadManager::new();
948        assert!(manager.find_by_name("nonexistent.txt").is_none());
949    }
950
951    #[test]
952    fn h0_file_52_download_manager_last_empty() {
953        let manager = DownloadManager::new();
954        assert!(manager.last().is_none());
955    }
956
957    #[test]
958    fn h0_file_53_download_manager_last_mut_empty() {
959        let mut manager = DownloadManager::new();
960        assert!(manager.last_mut().is_none());
961    }
962
963    #[test]
964    fn h0_file_54_is_accepted_all_wildcard() {
965        let chooser = FileChooser::new().accept(vec!["*/*"]);
966        let file = FileInput::text("test.txt", "");
967        assert!(chooser.is_accepted(&file));
968    }
969
970    #[test]
971    fn h0_file_55_guess_mime_htm() {
972        assert_eq!(guess_mime_type("page.htm"), "text/html");
973    }
974
975    #[test]
976    fn h0_file_56_guess_mime_gif() {
977        assert_eq!(guess_mime_type("animation.gif"), "image/gif");
978    }
979
980    #[test]
981    fn h0_file_57_guess_mime_webp() {
982        assert_eq!(guess_mime_type("image.webp"), "image/webp");
983    }
984
985    #[test]
986    fn h0_file_58_guess_mime_ico() {
987        assert_eq!(guess_mime_type("favicon.ico"), "image/x-icon");
988    }
989
990    #[test]
991    fn h0_file_59_guess_mime_audio() {
992        assert_eq!(guess_mime_type("song.mp3"), "audio/mpeg");
993        assert_eq!(guess_mime_type("sound.wav"), "audio/wav");
994    }
995
996    #[test]
997    fn h0_file_60_guess_mime_video() {
998        assert_eq!(guess_mime_type("movie.mp4"), "video/mp4");
999        assert_eq!(guess_mime_type("clip.webm"), "video/webm");
1000    }
1001
1002    #[test]
1003    fn h0_file_61_guess_mime_archive() {
1004        assert_eq!(guess_mime_type("archive.zip"), "application/zip");
1005        assert_eq!(guess_mime_type("archive.gz"), "application/gzip");
1006        assert_eq!(guess_mime_type("archive.tar"), "application/x-tar");
1007    }
1008
1009    #[test]
1010    fn h0_file_62_guess_mime_office() {
1011        assert_eq!(guess_mime_type("document.doc"), "application/msword");
1012        assert_eq!(
1013            guess_mime_type("document.docx"),
1014            "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
1015        );
1016        assert_eq!(
1017            guess_mime_type("spreadsheet.xls"),
1018            "application/vnd.ms-excel"
1019        );
1020        assert_eq!(
1021            guess_mime_type("spreadsheet.xlsx"),
1022            "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
1023        );
1024    }
1025
1026    #[test]
1027    fn h0_file_63_guess_mime_xml() {
1028        assert_eq!(guess_mime_type("data.xml"), "application/xml");
1029    }
1030
1031    #[test]
1032    fn h0_file_64_guess_mime_javascript() {
1033        assert_eq!(guess_mime_type("script.js"), "application/javascript");
1034    }
1035
1036    #[test]
1037    fn h0_file_65_file_input_path_accessor() {
1038        let file = FileInput::new("test.txt", "text/plain", vec![]);
1039        assert!(file.path.is_none());
1040
1041        let file_with_path = FileInput::from_path("folder/test.txt");
1042        assert!(file_with_path.path.is_some());
1043    }
1044
1045    #[test]
1046    fn h0_file_66_download_state_debug() {
1047        let states = vec![
1048            DownloadState::InProgress,
1049            DownloadState::Completed,
1050            DownloadState::Cancelled,
1051            DownloadState::Failed("error".to_string()),
1052            DownloadState::Deleted,
1053        ];
1054
1055        for state in &states {
1056            // Test Debug impl
1057            let _ = format!("{:?}", state);
1058        }
1059
1060        // Test PartialEq
1061        assert_eq!(DownloadState::InProgress, DownloadState::InProgress);
1062        assert_ne!(DownloadState::InProgress, DownloadState::Completed);
1063    }
1064
1065    #[test]
1066    fn h0_file_67_file_input_debug() {
1067        let file = FileInput::text("test.txt", "content");
1068        let debug_str = format!("{:?}", file);
1069        assert!(debug_str.contains("test.txt"));
1070    }
1071
1072    #[test]
1073    fn h0_file_68_download_debug() {
1074        let download = Download::new("http://test", "file.txt");
1075        let debug_str = format!("{:?}", download);
1076        assert!(debug_str.contains("file.txt"));
1077    }
1078
1079    #[test]
1080    fn h0_file_69_file_chooser_debug() {
1081        let chooser = FileChooser::new();
1082        let debug_str = format!("{:?}", chooser);
1083        assert!(debug_str.contains("FileChooser"));
1084    }
1085
1086    #[test]
1087    fn h0_file_70_download_manager_debug() {
1088        let manager = DownloadManager::new();
1089        let debug_str = format!("{:?}", manager);
1090        assert!(debug_str.contains("DownloadManager"));
1091    }
1092
1093    #[test]
1094    fn h0_file_71_set_files_single_mode_empty() {
1095        let mut chooser = FileChooser::single();
1096        chooser.set_files(Vec::<FileInput>::new());
1097        assert_eq!(chooser.file_count(), 0);
1098    }
1099
1100    #[test]
1101    fn h0_file_72_set_files_single_mode_exactly_one() {
1102        let mut chooser = FileChooser::single();
1103        chooser.set_files(vec![FileInput::text("single.txt", "content")]);
1104        assert_eq!(chooser.file_count(), 1);
1105        assert_eq!(chooser.files()[0].name(), "single.txt");
1106    }
1107
1108    #[test]
1109    fn h0_file_73_is_accepted_extension_case_insensitive() {
1110        let chooser = FileChooser::new().accept(vec![".pdf"]);
1111        let file = FileInput::from_path("doc.PDF");
1112        assert!(chooser.is_accepted(&file));
1113    }
1114
1115    #[test]
1116    fn h0_file_74_is_accepted_mime_wildcard_image() {
1117        let chooser = FileChooser::new().accept(vec!["image/*"]);
1118
1119        let gif = FileInput::new("test.gif", "image/gif", vec![]);
1120        assert!(chooser.is_accepted(&gif));
1121
1122        let webp = FileInput::new("test.webp", "image/webp", vec![]);
1123        assert!(chooser.is_accepted(&webp));
1124    }
1125
1126    #[test]
1127    fn h0_file_75_is_accepted_mime_wildcard_audio() {
1128        let chooser = FileChooser::new().accept(vec!["audio/*"]);
1129
1130        let mp3 = FileInput::new("test.mp3", "audio/mpeg", vec![]);
1131        assert!(chooser.is_accepted(&mp3));
1132
1133        let txt = FileInput::text("test.txt", "");
1134        assert!(!chooser.is_accepted(&txt));
1135    }
1136
1137    #[test]
1138    fn h0_file_76_download_path_none_when_not_saved() {
1139        let download = Download::new("http://test", "file.txt");
1140        assert!(download.path().is_none());
1141    }
1142
1143    #[test]
1144    fn h0_file_77_set_input_files_single_mode() {
1145        let mut chooser = FileChooser::single();
1146        chooser.set_input_files(&["file1.txt", "file2.txt"]);
1147        // Single mode should only keep first file
1148        assert_eq!(chooser.file_count(), 1);
1149        assert_eq!(chooser.files()[0].name(), "file1.txt");
1150    }
1151
1152    #[test]
1153    fn h0_file_78_file_input_serialize_deserialize() {
1154        let file = FileInput::text("test.txt", "content");
1155        let json = serde_json::to_string(&file).unwrap();
1156        let deserialized: FileInput = serde_json::from_str(&json).unwrap();
1157        assert_eq!(deserialized.name(), "test.txt");
1158        assert_eq!(deserialized.contents_string(), Some("content".to_string()));
1159    }
1160
1161    #[test]
1162    fn h0_file_79_download_serialize_deserialize() {
1163        let download = Download::completed("http://test", "file.txt", b"data".to_vec());
1164        let json = serde_json::to_string(&download).unwrap();
1165        let deserialized: Download = serde_json::from_str(&json).unwrap();
1166        assert_eq!(deserialized.suggested_filename(), "file.txt");
1167        assert!(deserialized.is_complete());
1168    }
1169
1170    #[test]
1171    fn h0_file_80_download_state_serialize_deserialize() {
1172        let states = vec![
1173            DownloadState::InProgress,
1174            DownloadState::Completed,
1175            DownloadState::Cancelled,
1176            DownloadState::Failed("Network error".to_string()),
1177            DownloadState::Deleted,
1178        ];
1179
1180        for state in states {
1181            let json = serde_json::to_string(&state).unwrap();
1182            let deserialized: DownloadState = serde_json::from_str(&json).unwrap();
1183            assert_eq!(state, deserialized);
1184        }
1185    }
1186}