rusty_commit_saver/
config.rs

1use log::{error, info};
2
3use std::{
4    fs,
5    path::{Path, PathBuf},
6};
7
8use clap::Parser;
9use configparser::ini::Ini;
10use dirs::home_dir;
11use once_cell::sync::OnceCell;
12
13/// Parses INI file content into a configuration object without file I/O.
14///
15/// This is a pure function that takes raw INI text and parses it into an `Ini` struct.
16/// It's useful for testing configuration parsing logic without reading from disk.
17///
18/// # Arguments
19///
20/// * `content` - The raw INI file content as a string
21///
22/// # Returns
23///
24/// - `Ok(Ini)` - Successfully parsed configuration
25/// - `Err(String)` - Parsing failed with error description
26///
27/// # INI Format
28///
29/// The INI format supported:
30/// ```text
31/// [section_name]
32/// key1 = value1
33/// key2 = value2
34///
35/// [another_section]
36/// key3 = value3
37/// ```
38///
39/// # Examples
40///
41/// ```ignore
42/// use rusty_commit_saver::config::parse_ini_content;
43///
44/// let ini_content = r#"
45/// [obsidian]
46/// root_path_dir = ~/Documents/Obsidian
47/// commit_path = Diaries/Commits
48///
49/// [templates]
50/// commit_date_path = %Y/%m-%B/%F.md
51/// commit_datetime = %Y-%m-%d %H:%M:%S
52/// "#;
53///
54/// let config = parse_ini_content(ini_content).unwrap();
55///
56/// // Access parsed values
57/// assert_eq!(
58///     config.get("obsidian", "root_path_dir"),
59///     Some("~/Documents/Obsidian".to_string())
60/// );
61/// assert_eq!(
62///     config.get("templates", "commit_date_path"),
63///     Some("%Y/%m-%B/%F.md".to_string())
64/// );
65/// ```
66///
67/// # Errors
68///
69/// Returns an error if:
70/// - INI syntax is invalid (malformed sections or key-value pairs)
71/// - The content cannot be parsed as valid UTF-8
72///
73/// # Testing
74///
75/// This function is particularly useful for unit testing without needing
76/// to create temporary files:
77///
78/// ```ignore
79/// use rusty_commit_saver::config::parse_ini_content;
80///
81/// fn test_config_parsing() {
82///     let test_config = "[section]\nkey=value\n";
83///     let result = parse_ini_content(test_config);
84///     assert!(result.is_ok());
85/// }
86/// ```
87pub fn parse_ini_content(content: &str) -> Result<Ini, String> {
88    let mut config = Ini::new();
89    config
90        .read(content.to_string())
91        .map_err(|e| format!("Failed to parse INI: {e:?}"))?;
92    Ok(config)
93}
94
95/// Thread-safe global configuration container for Rusty Commit Saver.
96///
97/// This struct holds all runtime configuration loaded from the INI file,
98/// using `OnceCell` for lazy initialization and thread safety. Configuration
99/// values are set once during initialization and remain immutable thereafter.
100///
101/// # Usage Pattern
102///
103/// ```ignore
104/// use rusty_commit_saver::config::GlobalVars;
105///
106/// // 1. Create instance
107/// let global_vars = GlobalVars::new();
108///
109/// // 2. Load configuration from INI file
110/// global_vars.set_all();
111///
112/// // 3. Access configuration values
113/// let obsidian_root = global_vars.get_obsidian_root_path_dir();
114/// let commit_path = global_vars.get_obsidian_commit_path();
115/// ```
116///
117/// # See Also
118///
119/// - [`GlobalVars::new()`] - Create new instance
120/// - [`GlobalVars::set_all()`] - Initialize from INI file
121/// - [`parse_ini_content()`] - Parse INI content
122#[derive(Debug, Default)]
123pub struct GlobalVars {
124    /// The parsed INI configuration file.
125    ///
126    /// Stores the complete parsed configuration from the INI file.
127    /// Initialized once by [`set_all()`](Self::set_all).
128    ///
129    /// # Thread Safety
130    ///
131    /// `OnceCell` ensures this is set exactly once and can be safely
132    /// accessed from multiple threads.
133    pub config: OnceCell<Ini>,
134
135    /// Root directory of the Obsidian vault.
136    ///
137    /// The base directory where all Obsidian files are stored.
138    /// All diary entries are created under this directory.
139    ///
140    /// # Examples
141    ///
142    /// - `/home/user/Documents/Obsidian`
143    /// - `C:\Users\username\Documents\Obsidian` (Windows)
144    ///
145    /// # Configuration
146    ///
147    /// Loaded from INI file:
148    /// ```text
149    /// [obsidian]
150    /// root_path_dir = ~/Documents/Obsidian
151    /// ```
152    obsidian_root_path_dir: OnceCell<PathBuf>,
153
154    /// Subdirectory path for commit diary entries.
155    ///
156    /// Relative path under [`obsidian_root_path_dir`](Self::obsidian_root_path_dir)
157    /// where commit entries are organized.
158    ///
159    /// # Examples
160    ///
161    /// - `Diaries/Commits`
162    /// - `Journal/Git`
163    ///
164    /// # Full Path Construction
165    ///
166    /// Combined with root and date template:
167    /// ```text
168    /// {root_path_dir}/{commit_path}/{date_template}
169    /// /home/user/Obsidian/Diaries/Commits/2025/01-January/2025-01-14.md
170    /// ```
171    ///
172    /// # Configuration
173    ///
174    /// Loaded from INI file:
175    /// ```text
176    /// [obsidian]
177    /// commit_path = Diaries/Commits
178    /// ```
179    obsidian_commit_path: OnceCell<PathBuf>,
180
181    /// Chrono format string for date-based file paths.
182    ///
183    /// Controls the directory structure and filename for diary entries.
184    /// Uses Chrono format specifiers to create date-organized paths.
185    ///
186    /// # Format Specifiers
187    ///
188    /// - `%Y` - Year (e.g., `2025`)
189    /// - `%m` - Month number (e.g., `01`)
190    /// - `%B` - Full month name (e.g., `January`)
191    /// - `%F` - ISO 8601 date (e.g., `2025-01-14`)
192    /// - `%d` - Day of month (e.g., `14`)
193    ///
194    /// # Examples
195    ///
196    /// ```text
197    /// Format: %Y/%m-%B/%F.md
198    /// Result: 2025/01-January/2025-01-14.md
199    ///
200    /// Format: %Y/week-%W/%F.md
201    /// Result: 2025/week-02/2025-01-14.md
202    /// ```
203    ///
204    /// # Configuration
205    ///
206    /// Loaded from INI file:
207    /// ```text
208    /// [templates]
209    /// commit_date_path = %Y/%m-%B/%F.md
210    /// ```
211    template_commit_date_path: OnceCell<String>,
212
213    /// Chrono format string for datetime display in diary entries.
214    ///
215    /// Controls how commit timestamps appear in the diary table's TIME column.
216    ///
217    /// # Format Specifiers
218    ///
219    /// - `%Y` - Year (e.g., `2025`)
220    /// - `%m` - Month (e.g., `01`)
221    /// - `%d` - Day (e.g., `14`)
222    /// - `%H` - Hour, 24-hour (e.g., `14`)
223    /// - `%M` - Minute (e.g., `30`)
224    /// - `%S` - Second (e.g., `45`)
225    /// - `%T` - Time in HH:MM:SS format
226    ///
227    /// # Examples
228    ///
229    /// ```text
230    /// Format: %Y-%m-%d %H:%M:%S
231    /// Result: 2025-01-14 14:30:45
232    ///
233    /// Format: %H:%M:%S
234    /// Result: 14:30:45
235    /// ```
236    ///
237    /// # Configuration
238    ///
239    /// Loaded from INI file:
240    /// ```text
241    /// [templates]
242    /// commit_datetime = %Y-%m-%d %H:%M:%S
243    /// ```
244    template_commit_datetime: OnceCell<String>,
245}
246
247impl GlobalVars {
248    /// Creates a new uninitialized `GlobalVars` instance.
249    ///
250    /// This constructor initializes all fields as empty `OnceCell` values.
251    /// Use [`set_all()`](Self::set_all) to load configuration from the INI file.
252    ///
253    /// # Thread Safety
254    ///
255    /// `GlobalVars` uses `OnceCell` for thread-safe, lazy initialization.
256    /// Configuration values are set once and cannot be changed afterward.
257    ///
258    /// # Returns
259    ///
260    /// A new `GlobalVars` instance with all fields uninitialized
261    ///
262    /// # Fields
263    ///
264    /// - `config` - The parsed INI configuration file
265    /// - `obsidian_root_path_dir` - Root directory of Obsidian vault
266    /// - `obsidian_commit_path` - Subdirectory path for commit entries
267    /// - `template_commit_date_path` - Chrono format for date-based directory structure
268    /// - `template_commit_datetime` - Chrono format for datetime strings
269    ///
270    /// # Examples
271    ///
272    /// ```ignore
273    /// use rusty_commit_saver::config::GlobalVars;
274    ///
275    /// // Create new instance
276    /// let global_vars = GlobalVars::new();
277    ///
278    /// // Now call set_all() to initialize from config file
279    /// // global_vars.set_all();
280    /// ```
281    #[must_use]
282    pub fn new() -> Self {
283        info!("[GlobalVars::new()] Creating new GlobalVars with OnceCell default values.");
284        GlobalVars {
285            config: OnceCell::new(),
286
287            obsidian_root_path_dir: OnceCell::new(),
288            obsidian_commit_path: OnceCell::new(),
289
290            template_commit_date_path: OnceCell::new(),
291            template_commit_datetime: OnceCell::new(),
292        }
293    }
294
295    /// Loads and initializes all configuration from the INI file.
296    ///
297    /// This is the main entry point for configuration setup. It:
298    /// 1. Reads the INI configuration file from disk (or CLI argument)
299    /// 2. Parses it into the `config` field
300    /// 3. Extracts and initializes all Obsidian and template variables
301    ///
302    /// Configuration is loaded from (in order of preference):
303    /// - `--config-ini <PATH>` CLI argument
304    /// - Default: `~/.config/rusty-commit-saver/rusty-commit-saver.ini`
305    ///
306    /// # Panics
307    ///
308    /// Panics if:
309    /// - Configuration file doesn't exist
310    /// - Configuration file cannot be read
311    /// - Configuration file has invalid INI format
312    /// - Required sections or keys are missing
313    /// - Section count is not exactly 2 (obsidian + templates)
314    ///
315    /// # Returns
316    ///
317    /// Returns `self` for method chaining
318    ///
319    /// # Required INI Structure
320    ///
321    /// ```text
322    /// [obsidian]
323    /// root_path_dir = ~/Documents/Obsidian
324    /// commit_path = Diaries/Commits
325    ///
326    /// [templates]
327    /// commit_date_path = %Y/%m-%B/%F.md
328    /// commit_datetime = %Y-%m-%d %H:%M:%S
329    /// ```
330    ///
331    /// # Examples
332    ///
333    /// ```ignore
334    /// use rusty_commit_saver::config::GlobalVars;
335    ///
336    /// let global_vars = GlobalVars::new();
337    /// global_vars.set_all(); // Reads from default or CLI config
338    ///
339    /// // Now all getters will return values
340    /// let root_path = global_vars.get_obsidian_root_path_dir();
341    /// let commit_path = global_vars.get_obsidian_commit_path();
342    /// ```
343    pub fn set_all(&self) -> &Self {
344        info!("[GlobalVars::set_all()] Setting all variables for GlobalVars");
345        let config = get_ini_file();
346
347        info!("[GlobalVars::set_all()]: Setting Config Ini file.");
348        self.config
349            .set(config)
350            .expect("Coulnd't set config in GlobalVars");
351
352        info!("[GlobalVars::set_all()]: Setting Obsidian variables from file.");
353        self.set_obsidian_vars();
354
355        self
356    }
357
358    /// Returns the root directory of the Obsidian vault.
359    ///
360    /// This is the base directory where all Obsidian vault files are stored.
361    /// All diary entries are created under this directory according to the
362    /// configured subdirectory structure.
363    ///
364    /// # Panics
365    ///
366    /// Panics if called before [`set_all()`](Self::set_all) has been invoked
367    ///
368    /// # Returns
369    ///
370    /// A `PathBuf` representing the Obsidian vault root directory
371    ///
372    /// # Examples
373    ///
374    /// ```ignore
375    /// use rusty_commit_saver::config::GlobalVars;
376    ///
377    /// let global_vars = GlobalVars::new();
378    /// global_vars.set_all();
379    ///
380    /// let root = global_vars.get_obsidian_root_path_dir();
381    /// println!("Obsidian vault root: {}", root.display());
382    /// // Output: Obsidian vault root: /home/user/Documents/Obsidian
383    /// ```
384    ///
385    /// # Configuration Source
386    ///
387    /// Read from INI file:
388    /// ```text
389    /// [obsidian]
390    /// root_path_dir = ~/Documents/Obsidian
391    /// ```
392    pub fn get_obsidian_root_path_dir(&self) -> PathBuf {
393        info!("[GlobalVars::get_obsidian_root_path_dir()]: Getting obsidian_root_path_dir.");
394        self.obsidian_root_path_dir
395            .get()
396            .expect("Could not get obsidian_root_path_dir")
397            .clone()
398    }
399
400    /// Returns the subdirectory path where commits are stored.
401    ///
402    /// This is a relative path under [`get_obsidian_root_path_dir()`](Self::get_obsidian_root_path_dir)
403    /// where commit diary entries will be organized. The full path is constructed by
404    /// combining this with the Obsidian root and the date-based directory structure.
405    ///
406    /// # Panics
407    ///
408    /// Panics if called before [`set_all()`](Self::set_all) has been invoked
409    ///
410    /// # Returns
411    ///
412    /// A `PathBuf` representing the commits subdirectory (relative path)
413    ///
414    /// # Examples
415    ///
416    /// ```ignore
417    /// use rusty_commit_saver::config::GlobalVars;
418    ///
419    /// let global_vars = GlobalVars::new();
420    /// global_vars.set_all();
421    ///
422    /// let commit_path = global_vars.get_obsidian_commit_path();
423    /// println!("Commit subdirectory: {}", commit_path.display());
424    /// // Output: Commit subdirectory: Diaries/Commits
425    ///
426    /// // Full path would be constructed as:
427    /// // /home/user/Documents/Obsidian/Diaries/Commits/2025/01-January/2025-01-14.md
428    /// ```
429    ///
430    /// # Configuration Source
431    ///
432    /// Read from INI file:
433    /// ```text
434    /// [obsidian]
435    /// commit_path = Diaries/Commits
436    /// ```
437    pub fn get_obsidian_commit_path(&self) -> PathBuf {
438        info!("[GlobalVars::get_obsidian_commit_path()]: Getting obsidian_commit_path.");
439        self.obsidian_commit_path
440            .get()
441            .expect("Could not get obsidian_commit_path")
442            .clone()
443    }
444
445    /// Returns the Chrono format string for diary file date hierarchies.
446    ///
447    /// This format string is used to create the directory structure and filename
448    /// for diary entries based on the commit timestamp. It controls how commits
449    /// are organized by date.
450    ///
451    /// # Chrono Format Specifiers
452    ///
453    /// - `%Y` - Full year (e.g., `2025`)
454    /// - `%m` - Month as zero-padded number (e.g., `01`)
455    /// - `%B` - Full month name (e.g., `January`)
456    /// - `%b` - Abbreviated month (e.g., `Jan`)
457    /// - `%d` - Day of month, zero-padded (e.g., `14`)
458    /// - `%F` - ISO 8601 date (equivalent to `%Y-%m-%d`, e.g., `2025-01-14`)
459    /// - `%H` - Hour in 24-hour format (e.g., `14`)
460    /// - `%M` - Minute (e.g., `30`)
461    /// - `%S` - Second (e.g., `45`)
462    ///
463    /// # Panics
464    ///
465    /// Panics if called before [`set_all()`](Self::set_all) has been invoked
466    ///
467    /// # Returns
468    ///
469    /// A `String` containing the Chrono format specifiers
470    ///
471    /// # Examples
472    ///
473    /// ```ignore
474    /// use rusty_commit_saver::config::GlobalVars;
475    ///
476    /// let global_vars = GlobalVars::new();
477    /// global_vars.set_all();
478    ///
479    /// let date_template = global_vars.get_template_commit_date_path();
480    /// println!("Date format: {}", date_template);
481    /// // Output: Date format: %Y/%m-%B/%F.md
482    ///
483    /// // This creates paths like:
484    /// // /home/user/Obsidian/Diaries/Commits/2025/01-January/2025-01-14.md
485    /// ```
486    ///
487    /// # Configuration Source
488    ///
489    /// Read from INI file:
490    /// ```text
491    /// [templates]
492    /// commit_date_path = %Y/%m-%B/%F.md
493    /// ```
494    pub fn get_template_commit_date_path(&self) -> String {
495        info!("[GlobalVars::get_template_commit_date_path()]: Getting template_commit_date_path.");
496        self.template_commit_date_path
497            .get()
498            .expect("Could not get template_commit_date_path")
499            .clone()
500    }
501
502    /// Returns the Chrono format string for commit timestamps in diary entries.
503    ///
504    /// This format string is used to display the commit time in the diary table.
505    /// It controls how timestamps appear in the commit entry rows.
506    ///
507    /// # Chrono Format Specifiers
508    ///
509    /// - `%Y` - Full year (e.g., `2025`)
510    /// - `%m` - Month as zero-padded number (e.g., `01`)
511    /// - `%B` - Full month name (e.g., `January`)
512    /// - `%d` - Day of month, zero-padded (e.g., `14`)
513    /// - `%H` - Hour in 24-hour format (e.g., `14`)
514    /// - `%M` - Minute, zero-padded (e.g., `30`)
515    /// - `%S` - Second, zero-padded (e.g., `45`)
516    /// - `%T` - Time in HH:MM:SS format (equivalent to `%H:%M:%S`)
517    ///
518    /// # Panics
519    ///
520    /// Panics if called before [`set_all()`](Self::set_all) has been invoked
521    ///
522    /// # Returns
523    ///
524    /// A `String` containing the Chrono format specifiers for datetime
525    ///
526    /// # Examples
527    ///
528    /// ```ignore
529    /// use rusty_commit_saver::config::GlobalVars;
530    ///
531    /// let global_vars = GlobalVars::new();
532    /// global_vars.set_all();
533    ///
534    /// let datetime_template = global_vars.get_template_commit_datetime();
535    /// println!("Datetime format: {}", datetime_template);
536    /// // Output: Datetime format: %Y-%m-%d %H:%M:%S
537    ///
538    /// // This renders timestamps like:
539    /// // 2025-01-14 14:30:45
540    /// ```
541    ///
542    /// # Diary Table Usage
543    ///
544    /// In the diary table, this format appears in the TIME column:
545    /// ```text
546    /// | FOLDER | TIME | COMMIT MESSAGE | REPOSITORY URL | BRANCH | COMMIT HASH |
547    /// |--------|------|----------------|----------------|--------|-------------|
548    /// | /work/project | 14:30:45 | feat: add feature | https://github.com/... | main | abc123... |
549    /// ```
550    ///
551    /// # Configuration Source
552    ///
553    /// Read from INI file:
554    /// ```text
555    /// [templates]
556    /// commit_datetime = %Y-%m-%d %H:%M:%S
557    /// ```
558    pub fn get_template_commit_datetime(&self) -> String {
559        info!("[GlobalVars::get_template_commit_datetime()]: Getting template_commit_datetime.");
560        self.template_commit_datetime
561            .get()
562            .expect("Could not get template_commit_datetime")
563            .clone()
564    }
565
566    /// Retrieves a clone of the parsed INI configuration.
567    ///
568    /// This is a private helper method that returns a copy of the configuration
569    /// object. Used internally by other helper methods to access sections and keys.
570    ///
571    /// # Panics
572    ///
573    /// Panics if called before [`set_all()`](Self::set_all) has initialized the config.
574    ///
575    /// # Returns
576    ///
577    /// A cloned `Ini` configuration object
578    fn get_config(&self) -> Ini {
579        info!("[GlobalVars::get_config()] Getting config");
580        self.config
581            .get()
582            .expect("Could not get Config. Config not initialized")
583            .clone()
584    }
585
586    fn get_key_from_section_from_ini(&self, section: &str, key: &str) -> Option<String> {
587        info!(
588            "[GlobalVars::get_key_from_section_from_ini()] Getting key: {key:} from section: {section:}."
589        );
590        self.config
591            .get()
592            .expect("Retrieving the config for commit_path")
593            .get(section, key)
594    }
595
596    fn get_sections_from_config(&self) -> Vec<String> {
597        info!("[GlobalVars::get_sections_from_config()] Getting sections from config");
598        let sections = self.get_config().sections();
599
600        info!("[GlobalVars::get_sections_from_config()] Checking validity of number of sections.");
601        if sections.len() == 2 {
602            sections
603        } else {
604            error!(
605                "[GlobalVars::get_sections_from_config()] Sections Len must be 2, we have: {:}",
606                sections.len()
607            );
608            error!(
609                "[GlobalVars::get_sections_from_config()] These are the sections found: {sections:?}"
610            );
611            panic!(
612                "[GlobalVars::get_sections_from_config()] config has the wrong number of sections."
613            )
614        }
615    }
616
617    /// Loads all configuration variables from the "obsidian" and "templates" sections.
618    ///
619    /// This method iterates through all sections returned by `get_sections_from_config`.
620    /// For each recognized section, it initializes the corresponding runtime variables
621    /// by calling their dedicated setters:
622    ///
623    /// - For the **"obsidian"** section: calls `set_obsidian_root_path_dir` and `set_obsidian_commit_path`.
624    /// - For the **"templates"** section: calls `set_templates_commit_date_path` and `set_templates_datetime`.
625    ///
626    /// # Panics
627    ///
628    /// Panics if the INI file contains a section other than "obsidian" or "templates", as only these two sections are supported.
629    ///
630    /// # Logging
631    ///
632    /// - Logs an info message when applying each section.
633    /// - Logs an error right before panicking on unsupported sections.
634    ///
635    /// # Examples
636    ///
637    /// ```ignore
638    /// use rusty_commit_saver::config::GlobalVars;
639    /// let mut config = configparser::ini::Ini::new();
640    /// config.set("obsidian", "root_path_dir", Some("~/Obsidian".to_string()));
641    /// config.set("obsidian", "commit_path", Some("Diary/Commits".to_string()));
642    /// config.set("templates", "commit_date_path", Some("%Y-%m-%d.md".to_string()));
643    /// config.set("templates", "commit_datetime", Some("%Y-%m-%d %H:%M:%S".to_string()));
644    /// let global_vars = GlobalVars::new();
645    /// global_vars.config.set(config).unwrap();
646    /// global_vars.set_obsidian_vars();
647    /// ```
648    pub fn set_obsidian_vars(&self) {
649        for section in self.get_sections_from_config() {
650            if section == "obsidian" {
651                info!("[GlobalVars::set_obsidian_vars()] Setting 'obsidian' section variables.");
652                self.set_obsidian_root_path_dir(&section);
653                self.set_obsidian_commit_path(&section);
654            } else if section == "templates" {
655                info!("[GlobalVars::set_obsidian_vars()] Setting 'templates' section variables.");
656                self.set_templates_commit_date_path(&section);
657                self.set_templates_datetime(&section);
658            } else {
659                error!(
660                    "[GlobalVars::set_obsidian_vars()] Trying to set other sections is not supported."
661                );
662                panic!(
663                    "[GlobalVars::set_obsidian_vars()] Trying to set other sections is not supported."
664                )
665            }
666        }
667    }
668
669    /// Sets the `template_commit_datetime` field from the `[templates]` section.
670    ///
671    /// Reads the `commit_datetime` key from the INI file and stores it in the
672    /// `template_commit_datetime` `OnceCell`.
673    ///
674    /// # Arguments
675    ///
676    /// * `section` - Should be `"templates"` (validated by caller)
677    ///
678    /// # Panics
679    ///
680    /// Panics if:
681    /// - The `commit_datetime` key is missing from the INI section
682    /// - The `OnceCell` has already been set (called multiple times)
683    ///
684    /// # Expected INI Key
685    ///
686    /// ```text
687    /// [templates]
688    /// commit_datetime = %Y-%m-%d %H:%M:%S
689    /// ```
690    fn set_templates_datetime(&self, section: &str) {
691        info!("[GlobalVars::set_templates_datetime()]: Setting the templates_datetime.");
692        let key = self
693            .get_key_from_section_from_ini(section, "commit_datetime")
694            .expect("Could not get the commit_datetime from INI");
695
696        self.template_commit_datetime
697            .set(key)
698            .expect("Could not set the template_commit_datetime GlobalVars");
699    }
700
701    /// Sets the `template_commit_date_path` field from the `[templates]` section.
702    ///
703    /// Reads the `commit_date_path` key from the INI file and stores it in the
704    /// `template_commit_date_path` `OnceCell`.
705    ///
706    /// # Arguments
707    ///
708    /// * `section` - Should be `"templates"` (validated by caller)
709    ///
710    /// # Panics
711    ///
712    /// Panics if:
713    /// - The `commit_date_path` key is missing from the INI section
714    /// - The `OnceCell` has already been set (called multiple times)
715    ///
716    /// # Expected INI Key
717    ///
718    /// ```text
719    /// [templates]
720    /// commit_date_path = %Y/%m-%B/%F.md
721    /// ```
722    fn set_templates_commit_date_path(&self, section: &str) {
723        info!(
724            "[GlobalVars::set_templates_commit_date_path()]: Setting the template_commit_date_path."
725        );
726        let key = self
727            .get_key_from_section_from_ini(section, "commit_date_path")
728            .expect("Could not get the commit_date_path from INI");
729
730        self.template_commit_date_path
731            .set(key)
732            .expect("Could not set the template_commit_date_path in GlobalVars");
733    }
734
735    /// Sets the `obsidian_commit_path` field from the `[obsidian]` section.
736    ///
737    /// Reads the `commit_path` key, expands tilde (`~`) to the home directory
738    /// if present, splits the path by `/`, and constructs a `PathBuf`.
739    ///
740    /// # Arguments
741    ///
742    /// * `section` - Should be `"obsidian"` (validated by caller)
743    ///
744    /// # Tilde Expansion
745    ///
746    /// - `~/Diaries/Commits` → `/home/user/Diaries/Commits`
747    /// - `/absolute/path` → `/absolute/path` (unchanged)
748    ///
749    /// # Panics
750    ///
751    /// Panics if:
752    /// - The `commit_path` key is missing from the INI section
753    /// - Home directory cannot be determined (when `~` is used)
754    /// - The `OnceCell` has already been set
755    ///
756    /// # Expected INI Key
757    ///
758    /// ```text
759    /// [obsidian]
760    /// commit_path = ~/Documents/Obsidian/Diaries/Commits
761    /// ```
762    fn set_obsidian_commit_path(&self, section: &str) {
763        let string_path = self
764            .get_key_from_section_from_ini(section, "commit_path")
765            .expect("Could not get commit_path from config");
766
767        let fixed_home = if string_path.contains('~') {
768            info!("[GlobalVars::set_obsidian_commit_path()]: Path does contain: '~'.");
769            set_proper_home_dir(&string_path)
770        } else {
771            info!("[GlobalVars::set_obsidian_commit_path()]: Path does NOT contain: '~'.");
772            string_path
773        };
774
775        let vec_str = fixed_home.split('/');
776
777        let mut path = PathBuf::new();
778
779        info!(
780            "[GlobalVars::set_obsidian_commit_path()]: Pushing strings folders to create PathBuf."
781        );
782        for s in vec_str {
783            path.push(s);
784        }
785        self.obsidian_commit_path
786            .set(path)
787            .expect("Could not set the path for obsidian_root_path_dir");
788    }
789
790    /// Sets the `obsidian_root_path_dir` field from the `[obsidian]` section.
791    ///
792    /// Reads the `root_path_dir` key, expands tilde (`~`) to the home directory
793    /// if present, prepends `/` for absolute paths, and constructs a `PathBuf`.
794    ///
795    /// # Arguments
796    ///
797    /// * `section` - Should be `"obsidian"` (validated by caller)
798    ///
799    /// # Path Construction
800    ///
801    /// - Starts with `/` to ensure absolute path
802    /// - Expands `~` to home directory
803    /// - Splits by `/` and constructs `PathBuf`
804    ///
805    /// # Tilde Expansion Examples
806    ///
807    /// - `~/Documents/Obsidian` → `/home/user/Documents/Obsidian`
808    /// - `/absolute/path` → `/absolute/path`
809    ///
810    /// # Panics
811    ///
812    /// Panics if:
813    /// - The `root_path_dir` key is missing from the INI section
814    /// - Home directory cannot be determined (when `~` is used)
815    /// - The `OnceCell` has already been set
816    ///
817    /// # Expected INI Key
818    ///
819    /// ```text
820    /// [obsidian]
821    /// root_path_dir = ~/Documents/Obsidian
822    /// ```
823    fn set_obsidian_root_path_dir(&self, section: &str) {
824        let string_path = self
825            .get_key_from_section_from_ini(section, "root_path_dir")
826            .expect("Could not get commit_path from config");
827
828        let fixed_home = if string_path.contains('~') {
829            info!("[GlobalVars::set_obsidian_root_path_dir()]: Does contain ~");
830            set_proper_home_dir(&string_path)
831        } else {
832            info!("[GlobalVars::set_obsidian_root_path_dir()]: Does NOT contain ~");
833            string_path
834        };
835
836        let vec_str = fixed_home.split('/');
837        let mut path = PathBuf::new();
838
839        info!(
840            "[GlobalVars::set_obsidian_root_path_dir()]: Pushing '/' to PathBuf for proper path."
841        );
842        path.push("/");
843
844        info!(
845            "[GlobalVars::set_obsidian_root_path_dir()]: Pushing strings folders to create PathBuf."
846        );
847        for s in vec_str {
848            path.push(s);
849        }
850
851        self.obsidian_root_path_dir
852            .set(path)
853            .expect("Could not set the path for obsidian_root_path_dir");
854    }
855}
856
857/// Command-line argument parser for configuration file path.
858///
859/// This struct uses `clap` to parse CLI arguments and provide configuration
860/// options for the application. Currently supports specifying a custom INI
861/// configuration file path.
862///
863/// # CLI Arguments
864///
865/// - `--config-ini <PATH>` - Optional path to a custom configuration file
866///
867/// # Examples
868///
869/// ```text
870/// # Use default config (~/.config/rusty-commit-saver/rusty-commit-saver.ini)
871/// rusty-commit-saver
872///
873/// # Use custom config file
874/// rusty-commit-saver --config-ini /path/to/custom.ini
875/// ```
876///
877/// # See Also
878///
879/// - [`retrieve_config_file_path()`] - Gets the config path from CLI or default
880/// - [`get_ini_file()`] - Loads the INI file from the resolved path
881#[derive(Parser, Debug, Clone)]
882#[command(version, about, long_about = None)]
883#[command(propagate_version = true)]
884#[command(about = "Rusty Commit Saver config", long_about = None)]
885pub struct UserInput {
886    /// Path to a custom INI configuration file.
887    ///
888    /// If not provided, the default configuration file is used:
889    /// `~/.config/rusty-commit-saver/rusty-commit-saver.ini`
890    ///
891    /// # CLI Usage
892    ///
893    /// ```text
894    /// rusty-commit-saver --config-ini /custom/path/config.ini
895    /// ```
896    ///
897    /// # Examples
898    ///
899    /// Valid paths:
900    /// - `~/my-configs/commit-saver.ini`
901    /// - `/etc/rusty-commit-saver/config.ini`
902    /// - `./local-config.ini`
903    #[arg(short, long)]
904    pub config_ini: Option<String>,
905}
906
907/// Retrieves the configuration file path from CLI arguments or returns the default.
908///
909/// This function parses command-line arguments and returns the path to the INI configuration file.
910/// If no `--config-ini` argument is provided, returns the default path.
911///
912/// # Default Path
913///
914/// `~/.config/rusty-commit-saver/rusty-commit-saver.ini`
915///
916/// # Returns
917///
918/// A `String` containing the absolute path to the configuration file.
919///
920/// # CLI Usage
921///
922/// ```text
923/// // Use default config
924/// $ rusty-commit-saver
925/// // Returns: ~/.config/rusty-commit-saver/rusty-commit-saver.ini
926///
927/// // Use custom config
928/// $ rusty-commit-saver --config-ini /custom/path/config.ini
929/// // Returns: /custom/path/config.ini
930/// ```
931///
932/// # Panics
933///
934/// Panics if:
935/// - The resolved configuration file does not exist on the filesystem
936/// - The file cannot be read (permission denied, IO error)
937/// - The file path cannot be converted to a valid string
938///
939/// # Examples
940///
941/// ```ignore
942/// use rusty_commit_saver::config::retrieve_config_file_path;
943///
944/// let config_path = retrieve_config_file_path();
945/// println!("Using config: {}", config_path);
946/// ```
947///
948/// # See Also
949///
950/// - [`get_or_default_config_ini_path`] - Helper that implements the CLI parsing logic
951/// - [`get_default_ini_path`] - Constructs the default configuration path
952#[must_use]
953pub fn retrieve_config_file_path() -> String {
954    info!(
955        "[UserInput::retrieve_config_file_path()]: retrieving the string path from CLI or default"
956    );
957    let config_path = get_or_default_config_ini_path();
958
959    if Path::new(&config_path).exists() {
960        info!("[UserInput::retrieve_config_file_path()]: config_path exists {config_path:}");
961    } else {
962        error!(
963            "[UserInput::retrieve_config_file_path()]: config_path DOES NOT exists {config_path:}"
964        );
965        panic!(
966            "[UserInput::retrieve_config_file_path()]: config_path DOES NOT exists {config_path:}"
967        );
968    }
969    info!("[UserInput::retrieve_config_file_path()] retrieved config path: {config_path:}");
970    fs::read_to_string(config_path.clone())
971        .unwrap_or_else(|_| panic!("Should have been able to read the file: {config_path:}"))
972}
973
974/// Returns the config path from CLI arguments or the default path.
975///
976/// Internal helper function that parses CLI arguments using `UserInput` and
977/// returns either the provided `--config-ini` path or the default configuration
978/// file location.
979///
980/// # Returns
981///
982/// - CLI path if `--config-ini` was provided
983/// - Default path (`~/.config/rusty-commit-saver/rusty-commit-saver.ini`) otherwise
984///
985/// # Called By
986///
987/// This function is called internally by [`retrieve_config_file_path()`].
988///
989/// # See Also
990///
991/// - [`get_default_ini_path()`] - Constructs the default configuration path
992#[must_use]
993pub fn get_or_default_config_ini_path() -> String {
994    info!("[get_or_default_config_ini_path()]: Parsing CLI inputs.");
995    let args = UserInput::parse();
996
997    let config_path = if let Some(cfg_str) = args.config_ini {
998        if cfg_str.contains('~') {
999            info!(
1000                "[get_or_default_config_ini_path()]: Configuration string exists and contains '~'."
1001            );
1002            set_proper_home_dir(&cfg_str)
1003        } else {
1004            info!(
1005                "[get_or_default_config_ini_path()]: Configuration string exists but does NOT contain: `~'."
1006            );
1007            cfg_str
1008        }
1009    } else {
1010        info!(
1011            "[get_or_default_config_ini_path()]: Configuration string does NOT exist, using default values."
1012        );
1013
1014        get_default_ini_path()
1015    };
1016
1017    info!("[get_or_default_config_ini_path()]: Config path found: {config_path:}");
1018    config_path
1019}
1020
1021/// Constructs the default configuration file path.
1022///
1023/// Builds the standard XDG configuration path for the application by combining
1024/// the user's home directory with the application-specific config directory.
1025///
1026/// # Returns
1027///
1028/// A `String` with the default INI file path:
1029/// `~/.config/rusty-commit-saver/rusty-commit-saver.ini`
1030///
1031/// # Directory Structure
1032///
1033/// ```text
1034/// ~/.config/
1035///   └── rusty-commit-saver/
1036///       └── rusty-commit-saver.ini
1037/// ```
1038///
1039/// # Panics
1040///
1041/// Panics if the user's home directory cannot be determined
1042/// (via the `dirs::home_dir()` function).
1043///
1044/// # Examples
1045///
1046/// ```ignore
1047/// // Internal usage
1048/// let default_path = get_default_ini_path();
1049/// // Returns: "/home/user/.config/rusty-commit-saver/rusty-commit-saver.ini"
1050/// ```
1051///
1052/// # See Also
1053///
1054/// - [`retrieve_config_file_path()`] - Public API for getting config path
1055#[must_use]
1056pub fn get_default_ini_path() -> String {
1057    info!("[get_default_ini_path()]: Getting default ini file.");
1058    let cfg_str = "~/.config/rusty-commit-saver/rusty-commit-saver.ini".to_string();
1059    set_proper_home_dir(&cfg_str)
1060}
1061
1062/// Loads and parses the INI configuration file from disk.
1063///
1064/// Reads the configuration file (from CLI argument or default location),
1065/// parses its contents using [`parse_ini_content()`], and returns the
1066/// parsed `Ini` object.
1067///
1068/// # Returns
1069///
1070/// A parsed `Ini` configuration object
1071///
1072/// # Panics
1073///
1074/// Panics if:
1075/// - The configuration file doesn't exist at the resolved path
1076/// - The file cannot be read (permission denied, I/O error)
1077/// - The file content is not valid UTF-8
1078/// - The INI syntax is invalid (malformed sections or key-value pairs)
1079///
1080/// # File Resolution Order
1081///
1082/// 1. Check for `--config-ini <PATH>` CLI argument
1083/// 2. Fall back to `~/.config/rusty-commit-saver/rusty-commit-saver.ini`
1084///
1085/// # Expected INI Structure
1086///
1087/// ```text
1088/// [obsidian]
1089/// root_path_dir = ~/Documents/Obsidian
1090/// commit_path = Diaries/Commits
1091///
1092/// [templates]
1093/// commit_date_path = %Y/%m-%B/%F.md
1094/// commit_datetime = %Y-%m-%d %H:%M:%S
1095/// ```
1096///
1097/// # Called By
1098///
1099/// This function is called internally by [`GlobalVars::set_all()`].
1100///
1101/// # See Also
1102///
1103/// - [`retrieve_config_file_path()`] - Resolves the config file path
1104/// - [`parse_ini_content()`] - Parses INI text into `Ini` struct
1105#[must_use]
1106pub fn get_ini_file() -> Ini {
1107    info!("[get_ini_file()]: Retrieving the INI File");
1108    let content_ini = retrieve_config_file_path();
1109    let mut config = Ini::new();
1110    config
1111        .read(content_ini)
1112        .expect("Could not read the INI file!");
1113
1114    info!("[get_ini_file()]: This is the INI File:\n\n{config:?}");
1115    config
1116}
1117
1118/// Expands the tilde (`~`) character to the user's home directory path.
1119///
1120/// Replaces the leading `~` in a path string with the absolute path to the
1121/// user's home directory. If no `~` is present, returns the string unchanged.
1122///
1123/// # Arguments
1124///
1125/// * `cfg_str` - A path string that may contain a leading `~`
1126///
1127/// # Returns
1128///
1129/// A `String` with `~` expanded to the full home directory path
1130///
1131/// # Panics
1132///
1133/// Panics if the user's home directory cannot be determined
1134/// (via the `dirs::home_dir()` function).
1135///
1136/// # Examples
1137///
1138/// ```ignore
1139/// // On Linux/macOS with home at /home/user
1140/// let expanded = set_proper_home_dir("~/Documents/Obsidian");
1141/// assert_eq!(expanded, "/home/user/Documents/Obsidian");
1142///
1143/// // Path without tilde is returned unchanged
1144/// let unchanged = set_proper_home_dir("/absolute/path");
1145/// assert_eq!(unchanged, "/absolute/path");
1146/// ```
1147///
1148/// # Platform Behavior
1149///
1150/// - **Linux/macOS**: Expands to `/home/username` or `/Users/username`
1151/// - **Windows**: Expands to `C:\Users\username`
1152///
1153/// # Used By
1154///
1155/// This function is called by:
1156/// - [`GlobalVars::set_obsidian_root_path_dir()`]
1157/// - [`GlobalVars::set_obsidian_commit_path()`]
1158fn set_proper_home_dir(cfg_str: &str) -> String {
1159    info!("[set_proper_home_dir()]: Changing the '~' to full home directory.");
1160    let home_dir = home_dir()
1161        .expect("Could not get home_dir")
1162        .into_os_string()
1163        .into_string()
1164        .expect("Could not convert home_dir from OsString to String");
1165
1166    cfg_str.replace('~', &home_dir)
1167}
1168
1169#[cfg(test)]
1170mod global_vars_tests {
1171    use super::*;
1172
1173    #[test]
1174    fn test_global_vars_new() {
1175        let global_vars = GlobalVars::new();
1176
1177        assert!(global_vars.config.get().is_none());
1178    }
1179
1180    #[test]
1181    fn test_global_vars_default() {
1182        let global_vars = GlobalVars::default();
1183
1184        assert!(global_vars.config.get().is_none());
1185    }
1186
1187    #[test]
1188    fn test_get_sections_from_config_valid() {
1189        let mut config = Ini::new();
1190        config.set("obsidian", "root_path_dir", Some("/tmp/test".to_string()));
1191        config.set(
1192            "templates",
1193            "commit_date_path",
1194            Some("%Y-%m-%d".to_string()),
1195        );
1196
1197        let global_vars = GlobalVars::new();
1198        global_vars.config.set(config).unwrap();
1199
1200        let sections = global_vars.get_sections_from_config();
1201
1202        assert_eq!(sections.len(), 2);
1203        assert!(sections.contains(&"obsidian".to_string()));
1204        assert!(sections.contains(&"templates".to_string()));
1205    }
1206
1207    #[test]
1208    #[should_panic(expected = "config has the wrong number of sections")]
1209    fn test_get_sections_from_config_invalid_count() {
1210        let mut config = Ini::new();
1211        config.set("only_one_section", "key", Some("value".to_string()));
1212
1213        let global_vars = GlobalVars::new();
1214        global_vars.config.set(config).unwrap();
1215
1216        // This should panic because we only have 1 section, not 2
1217        global_vars.get_sections_from_config();
1218    }
1219
1220    #[test]
1221    fn test_get_key_from_section_from_ini_exists() {
1222        let mut config = Ini::new();
1223        config.set(
1224            "obsidian",
1225            "root_path_dir",
1226            Some("/home/user/Obsidian".to_string()),
1227        );
1228
1229        let global_vars = GlobalVars::new();
1230        global_vars.config.set(config).unwrap();
1231
1232        let result = global_vars.get_key_from_section_from_ini("obsidian", "root_path_dir");
1233
1234        assert_eq!(result, Some("/home/user/Obsidian".to_string()));
1235    }
1236
1237    #[test]
1238    fn test_get_key_from_section_from_ini_not_exists() {
1239        let mut config = Ini::new();
1240        config.set("obsidian", "other_key", Some("value".to_string()));
1241
1242        let global_vars = GlobalVars::new();
1243        global_vars.config.set(config).unwrap();
1244
1245        let result = global_vars.get_key_from_section_from_ini("obsidian", "non_existent_key");
1246
1247        assert_eq!(result, None);
1248    }
1249
1250    #[test]
1251    fn test_get_config() {
1252        let mut config = Ini::new();
1253        config.set("test", "key", Some("value".to_string()));
1254
1255        let global_vars = GlobalVars::new();
1256        global_vars.config.set(config.clone()).unwrap();
1257
1258        let retrieved_config = global_vars.get_config();
1259
1260        assert_eq!(
1261            retrieved_config.get("test", "key"),
1262            Some("value".to_string())
1263        );
1264    }
1265
1266    #[test]
1267    fn test_set_obsidian_root_path_dir_with_tilde() {
1268        let mut config = Ini::new();
1269        config.set(
1270            "obsidian",
1271            "root_path_dir",
1272            Some("~/Documents/Obsidian".to_string()),
1273        );
1274        config.set(
1275            "templates",
1276            "commit_date_path",
1277            Some("%Y-%m-%d".to_string()),
1278        );
1279        config.set("templates", "commit_datetime", Some("%Y-%m-%d".to_string()));
1280
1281        let global_vars = GlobalVars::new();
1282        global_vars.config.set(config).unwrap();
1283        global_vars.set_obsidian_root_path_dir("obsidian");
1284
1285        let result = global_vars.get_obsidian_root_path_dir();
1286
1287        // Should expand ~ to full home path
1288        assert!(!result.to_string_lossy().contains('~'));
1289        // Should start with /
1290        assert!(result.to_string_lossy().starts_with('/'));
1291        // Should end with Obsidian
1292        assert!(result.to_string_lossy().ends_with("Obsidian"));
1293    }
1294
1295    #[test]
1296    fn test_set_obsidian_root_path_dir_absolute_path() {
1297        let mut config = Ini::new();
1298        config.set(
1299            "obsidian",
1300            "root_path_dir",
1301            Some("/absolute/path/Obsidian".to_string()),
1302        );
1303        config.set(
1304            "templates",
1305            "commit_date_path",
1306            Some("%Y-%m-%d".to_string()),
1307        );
1308        config.set("templates", "commit_datetime", Some("%Y-%m-%d".to_string()));
1309
1310        let global_vars = GlobalVars::new();
1311        global_vars.config.set(config).unwrap();
1312        global_vars.set_obsidian_root_path_dir("obsidian");
1313
1314        let result = global_vars.get_obsidian_root_path_dir();
1315
1316        // Should preserve absolute path
1317        assert!(result.to_string_lossy().contains("/absolute/path/Obsidian"));
1318    }
1319
1320    #[test]
1321    fn test_set_obsidian_commit_path_with_tilde() {
1322        let mut config = Ini::new();
1323        config.set(
1324            "obsidian",
1325            "commit_path",
1326            Some("~/Diaries/Commits".to_string()),
1327        );
1328        config.set(
1329            "templates",
1330            "commit_date_path",
1331            Some("%Y-%m-%d".to_string()),
1332        );
1333        config.set("templates", "commit_datetime", Some("%Y-%m-%d".to_string()));
1334
1335        let global_vars = GlobalVars::new();
1336        global_vars.config.set(config).unwrap();
1337        global_vars.set_obsidian_commit_path("obsidian");
1338
1339        let result = global_vars.get_obsidian_commit_path();
1340
1341        // Should expand ~ to full home path
1342        assert!(!result.to_string_lossy().contains('~'));
1343        // Should end with Commits
1344        assert!(result.to_string_lossy().ends_with("Commits"));
1345    }
1346
1347    #[test]
1348    fn test_set_obsidian_commit_path_absolute_path() {
1349        let mut config = Ini::new();
1350        config.set(
1351            "obsidian",
1352            "commit_path",
1353            Some("absolute/Diaries/Commits".to_string()),
1354        );
1355        config.set(
1356            "templates",
1357            "commit_date_path",
1358            Some("%Y-%m-%d".to_string()),
1359        );
1360        config.set("templates", "commit_datetime", Some("%Y-%m-%d".to_string()));
1361
1362        let global_vars = GlobalVars::new();
1363        global_vars.config.set(config).unwrap();
1364        global_vars.set_obsidian_commit_path("obsidian");
1365
1366        let result = global_vars.get_obsidian_commit_path();
1367
1368        // set_obsidian_commit_path() doesn't add leading / (unlike root_path_dir)
1369        // It just splits by / and rebuilds the PathBuf
1370        assert!(result.to_string_lossy().contains("absolute"));
1371        assert!(result.to_string_lossy().ends_with("Commits"));
1372    }
1373
1374    #[test]
1375    fn test_set_templates_commit_date_path() {
1376        let mut config = Ini::new();
1377        config.set(
1378            "templates",
1379            "commit_date_path",
1380            Some("%Y/%m-%B/%F.md".to_string()),
1381        );
1382        config.set("templates", "commit_datetime", Some("%Y-%m-%d".to_string()));
1383
1384        let global_vars = GlobalVars::new();
1385        global_vars.config.set(config).unwrap();
1386        global_vars.set_templates_commit_date_path("templates");
1387
1388        let result = global_vars.get_template_commit_date_path();
1389
1390        assert_eq!(result, "%Y/%m-%B/%F.md");
1391    }
1392
1393    #[test]
1394    fn test_set_templates_datetime() {
1395        let mut config = Ini::new();
1396        config.set(
1397            "templates",
1398            "commit_datetime",
1399            Some("%Y-%m-%d %H:%M:%S".to_string()),
1400        );
1401
1402        let global_vars = GlobalVars::new();
1403        global_vars.config.set(config).unwrap();
1404        global_vars.set_templates_datetime("templates");
1405
1406        let result = global_vars.get_template_commit_datetime();
1407
1408        assert_eq!(result, "%Y-%m-%d %H:%M:%S");
1409    }
1410
1411    #[test]
1412    fn test_set_obsidian_vars_both_sections() {
1413        let mut config = Ini::new();
1414        config.set(
1415            "obsidian",
1416            "root_path_dir",
1417            Some("/home/user/Obsidian".to_string()),
1418        );
1419        config.set(
1420            "obsidian",
1421            "commit_path",
1422            Some("Diaries/Commits".to_string()),
1423        );
1424        config.set(
1425            "templates",
1426            "commit_date_path",
1427            Some("%Y-%m-%d.md".to_string()),
1428        );
1429        config.set(
1430            "templates",
1431            "commit_datetime",
1432            Some("%Y-%m-%d %H:%M:%S".to_string()),
1433        );
1434
1435        let global_vars = GlobalVars::new();
1436        global_vars.config.set(config).unwrap();
1437
1438        // Call the private method indirectly through set_obsidian_vars
1439        global_vars.set_obsidian_vars();
1440
1441        // Verify all getters work (meaning setters were called)
1442        let root_path = global_vars.get_obsidian_root_path_dir();
1443        let commit_path = global_vars.get_obsidian_commit_path();
1444        let date_path = global_vars.get_template_commit_date_path();
1445        let datetime = global_vars.get_template_commit_datetime();
1446
1447        assert!(root_path.to_string_lossy().contains("Obsidian"));
1448        assert!(commit_path.to_string_lossy().contains("Commits"));
1449        assert_eq!(date_path, "%Y-%m-%d.md");
1450        assert_eq!(datetime, "%Y-%m-%d %H:%M:%S");
1451    }
1452
1453    #[test]
1454    #[should_panic(expected = "Trying to set other sections is not supported")]
1455    fn test_set_obsidian_vars_invalid_section() {
1456        let mut config = Ini::new();
1457        // Add correct number of sections (2) but with wrong name
1458        config.set("invalid_section", "key", Some("value".to_string()));
1459        config.set(
1460            "templates",
1461            "commit_date_path",
1462            Some("%Y-%m-%d.md".to_string()),
1463        );
1464        config.set(
1465            "templates",
1466            "commit_datetime",
1467            Some("%Y-%m-%d %H:%M".to_string()),
1468        );
1469
1470        let global_vars = GlobalVars::new();
1471        global_vars.config.set(config).unwrap();
1472
1473        // Should panic because "invalid_section" is not "obsidian" or "templates"
1474        global_vars.set_obsidian_vars();
1475    }
1476
1477    #[test]
1478    fn test_set_all_integration() {
1479        use std::io::Write;
1480        use tempfile::NamedTempFile;
1481
1482        // Create a temporary config file
1483        let mut temp_file = NamedTempFile::new().unwrap();
1484        writeln!(temp_file, "[obsidian]").unwrap();
1485        writeln!(temp_file, "root_path_dir=/tmp/test_obsidian").unwrap();
1486        writeln!(temp_file, "commit_path=TestDiaries/TestCommits").unwrap();
1487        writeln!(temp_file, "[templates]").unwrap();
1488        writeln!(temp_file, "commit_date_path=%Y-%m-%d.md").unwrap();
1489        writeln!(temp_file, "commit_datetime=%Y-%m-%d %H:%M:%S").unwrap();
1490        temp_file.flush().unwrap();
1491
1492        // Parse the config manually and test set_all
1493        let content = std::fs::read_to_string(temp_file.path()).unwrap();
1494        let config = parse_ini_content(&content).unwrap();
1495
1496        let global_vars = GlobalVars::new();
1497        global_vars.config.set(config).unwrap();
1498        global_vars.set_obsidian_vars();
1499
1500        // Verify all values were set
1501        let root = global_vars.get_obsidian_root_path_dir();
1502        let commit = global_vars.get_obsidian_commit_path();
1503        let date = global_vars.get_template_commit_date_path();
1504        let datetime = global_vars.get_template_commit_datetime();
1505
1506        assert!(root.to_string_lossy().contains("test_obsidian"));
1507        assert!(commit.to_string_lossy().contains("TestCommits"));
1508        assert_eq!(date, "%Y-%m-%d.md");
1509        assert_eq!(datetime, "%Y-%m-%d %H:%M:%S");
1510    }
1511
1512    #[test]
1513    #[should_panic(expected = "Could not get")]
1514    fn test_get_obsidian_root_path_dir_not_set() {
1515        let global_vars = GlobalVars::new();
1516        // Don't set any values
1517        // This should panic when trying to get
1518        global_vars.get_obsidian_root_path_dir();
1519    }
1520
1521    #[test]
1522    #[should_panic(expected = "Could not get")]
1523    fn test_get_obsidian_commit_path_not_set() {
1524        let global_vars = GlobalVars::new();
1525        global_vars.get_obsidian_commit_path();
1526    }
1527
1528    #[test]
1529    #[should_panic(expected = "Could not get")]
1530    fn test_get_template_commit_date_path_not_set() {
1531        let global_vars = GlobalVars::new();
1532        global_vars.get_template_commit_date_path();
1533    }
1534
1535    #[test]
1536    #[should_panic(expected = "Could not get")]
1537    fn test_get_template_commit_datetime_not_set() {
1538        let global_vars = GlobalVars::new();
1539        global_vars.get_template_commit_datetime();
1540    }
1541
1542    #[test]
1543    #[should_panic(expected = "Could not get Config")]
1544    fn test_get_config_not_initialized() {
1545        let global_vars = GlobalVars::new();
1546        // Config not set
1547        global_vars.get_config();
1548    }
1549
1550    #[test]
1551    fn test_set_config_twice_fails() {
1552        let global_vars = GlobalVars::new();
1553        let config1 = Ini::new();
1554        let config2 = Ini::new();
1555
1556        assert!(global_vars.config.set(config1).is_ok());
1557        // Second set should fail
1558        assert!(global_vars.config.set(config2).is_err());
1559    }
1560
1561    #[test]
1562    fn test_global_vars_set_all_end_to_end() {
1563        use std::io::Write;
1564        use tempfile::NamedTempFile;
1565
1566        // Create a real config file
1567        let mut temp_file = NamedTempFile::new().unwrap();
1568        writeln!(temp_file, "[obsidian]").unwrap();
1569        writeln!(temp_file, "root_path_dir=/tmp/obsidian_test").unwrap();
1570        writeln!(temp_file, "commit_path=TestDiaries/TestCommits").unwrap();
1571        writeln!(temp_file, "[templates]").unwrap();
1572        writeln!(temp_file, "commit_date_path=%Y/%m-%B/%F.md").unwrap();
1573        writeln!(temp_file, "commit_datetime=%Y-%m-%d %H:%M:%S").unwrap();
1574        temp_file.flush().unwrap();
1575
1576        // Read and parse the config
1577        let content = std::fs::read_to_string(temp_file.path()).unwrap();
1578        let mut config = Ini::new();
1579        config.read(content).unwrap();
1580
1581        // Now test set_all
1582        let global_vars = GlobalVars::new();
1583        let result = global_vars.config.set(config);
1584        assert!(result.is_ok());
1585
1586        // Call set_obsidian_vars (which set_all would call)
1587        global_vars.set_obsidian_vars();
1588
1589        // Verify everything is accessible
1590        let root = global_vars.get_obsidian_root_path_dir();
1591        let commit = global_vars.get_obsidian_commit_path();
1592        let date_path = global_vars.get_template_commit_date_path();
1593        let datetime = global_vars.get_template_commit_datetime();
1594
1595        assert!(root.to_string_lossy().contains("obsidian_test"));
1596        assert!(commit.to_string_lossy().contains("TestCommits"));
1597        assert_eq!(date_path, "%Y/%m-%B/%F.md");
1598        assert_eq!(datetime, "%Y-%m-%d %H:%M:%S");
1599    }
1600
1601    #[test]
1602    fn test_set_obsidian_root_path_dir_with_trailing_slash() {
1603        let mut config = Ini::new();
1604        config.set("obsidian", "root_path_dir", Some("/tmp/test/".to_string()));
1605        config.set(
1606            "templates",
1607            "commit_date_path",
1608            Some("%Y-%m-%d".to_string()),
1609        );
1610        config.set("templates", "commit_datetime", Some("%Y-%m-%d".to_string()));
1611
1612        let global_vars = GlobalVars::new();
1613        global_vars.config.set(config).unwrap();
1614        global_vars.set_obsidian_root_path_dir("obsidian");
1615
1616        let result = global_vars.get_obsidian_root_path_dir();
1617
1618        // Should handle trailing slashes gracefully
1619        assert!(result.to_string_lossy().contains("test"));
1620    }
1621
1622    #[test]
1623    fn test_set_obsidian_commit_path_with_multiple_slashes() {
1624        let mut config = Ini::new();
1625        config.set(
1626            "obsidian",
1627            "commit_path",
1628            Some("Diaries//Commits///Nested".to_string()),
1629        );
1630        config.set(
1631            "templates",
1632            "commit_date_path",
1633            Some("%Y-%m-%d".to_string()),
1634        );
1635        config.set("templates", "commit_datetime", Some("%Y-%m-%d".to_string()));
1636
1637        let global_vars = GlobalVars::new();
1638        global_vars.config.set(config).unwrap();
1639        global_vars.set_obsidian_commit_path("obsidian");
1640
1641        let result = global_vars.get_obsidian_commit_path();
1642
1643        // Path should be constructed despite multiple slashes
1644        assert!(result.to_string_lossy().contains("Nested"));
1645    }
1646
1647    #[test]
1648    fn test_set_obsidian_root_path_dir_empty_string() {
1649        let mut config = Ini::new();
1650        config.set("obsidian", "root_path_dir", Some(String::new()));
1651        config.set(
1652            "templates",
1653            "commit_date_path",
1654            Some("%Y-%m-%d".to_string()),
1655        );
1656        config.set("templates", "commit_datetime", Some("%Y-%m-%d".to_string()));
1657
1658        let global_vars = GlobalVars::new();
1659        global_vars.config.set(config).unwrap();
1660        global_vars.set_obsidian_root_path_dir("obsidian");
1661
1662        let result = global_vars.get_obsidian_root_path_dir();
1663
1664        // Should at least create a PathBuf (even if empty or just "/")
1665        assert!(!result.to_string_lossy().is_empty());
1666    }
1667
1668    #[test]
1669    #[should_panic(expected = "Could not get commit_path from config")]
1670    fn test_set_obsidian_commit_path_missing_key() {
1671        let mut config = Ini::new();
1672        config.set("obsidian", "root_path_dir", Some("/tmp/test".to_string()));
1673        config.set(
1674            "templates",
1675            "commit_date_path",
1676            Some("%Y-%m-%d".to_string()),
1677        );
1678        config.set("templates", "commit_datetime", Some("%Y-%m-%d".to_string()));
1679
1680        let global_vars = GlobalVars::new();
1681        global_vars.config.set(config).unwrap();
1682
1683        global_vars.set_obsidian_commit_path("obsidian");
1684    }
1685
1686    #[test]
1687    #[should_panic(expected = "Could not get")]
1688    fn test_set_obsidian_root_path_dir_missing_key() {
1689        let mut config = Ini::new();
1690        config.set("obsidian", "commit_path", Some("commits".to_string()));
1691        config.set(
1692            "templates",
1693            "commit_date_path",
1694            Some("%Y-%m-%d".to_string()),
1695        );
1696        config.set("templates", "commit_datetime", Some("%Y-%m-%d".to_string()));
1697
1698        let global_vars = GlobalVars::new();
1699        global_vars.config.set(config).unwrap();
1700
1701        global_vars.set_obsidian_root_path_dir("obsidian");
1702    }
1703
1704    #[test]
1705    #[should_panic(expected = "Could not get the commit_date_path from INI")]
1706    fn test_set_templates_commit_date_path_missing_key() {
1707        let mut config = Ini::new();
1708        config.set("templates", "commit_datetime", Some("%Y-%m-%d".to_string()));
1709        config.set("obsidian", "root_path_dir", Some("/tmp".to_string()));
1710        config.set("obsidian", "commit_path", Some("commits".to_string()));
1711
1712        let global_vars = GlobalVars::new();
1713        global_vars.config.set(config).unwrap();
1714
1715        global_vars.set_templates_commit_date_path("templates");
1716    }
1717
1718    #[test]
1719    #[should_panic(expected = "Could not get the commit_datetime from INI")]
1720    fn test_set_templates_datetime_missing_key() {
1721        let mut config = Ini::new();
1722        config.set(
1723            "templates",
1724            "commit_date_path",
1725            Some("%Y-%m-%d".to_string()),
1726        );
1727        config.set("obsidian", "root_path_dir", Some("/tmp".to_string()));
1728        config.set("obsidian", "commit_path", Some("commits".to_string()));
1729
1730        let global_vars = GlobalVars::new();
1731        global_vars.config.set(config).unwrap();
1732
1733        global_vars.set_templates_datetime("templates");
1734    }
1735
1736    #[test]
1737    fn test_global_vars_set_all_method() {
1738        use std::io::Write;
1739        use tempfile::NamedTempFile;
1740
1741        // Create a real config file
1742        let mut temp_file = NamedTempFile::new().unwrap();
1743        writeln!(temp_file, "[obsidian]").unwrap();
1744        writeln!(temp_file, "root_path_dir=/tmp/obsidian_full_test").unwrap();
1745        writeln!(temp_file, "commit_path=FullTest/Commits").unwrap();
1746        writeln!(temp_file, "[templates]").unwrap();
1747        writeln!(temp_file, "commit_date_path=%Y/%m/%d.md").unwrap();
1748        writeln!(temp_file, "commit_datetime=%Y-%m-%d %H:%M:%S").unwrap();
1749        temp_file.flush().unwrap();
1750
1751        // Parse config manually
1752        let content = std::fs::read_to_string(temp_file.path()).unwrap();
1753        let config = parse_ini_content(&content).unwrap();
1754
1755        // Test set_all workflow
1756        let global_vars = GlobalVars::new();
1757        global_vars.config.set(config).unwrap();
1758        global_vars.set_obsidian_vars();
1759
1760        // Verify all values accessible via set_all pattern
1761        let root = global_vars.get_obsidian_root_path_dir();
1762        let commit = global_vars.get_obsidian_commit_path();
1763        let date = global_vars.get_template_commit_date_path();
1764        let datetime = global_vars.get_template_commit_datetime();
1765
1766        assert!(root.to_string_lossy().contains("obsidian_full_test"));
1767        assert!(commit.to_string_lossy().contains("FullTest"));
1768        assert_eq!(date, "%Y/%m/%d.md");
1769        assert_eq!(datetime, "%Y-%m-%d %H:%M:%S");
1770    }
1771
1772    #[test]
1773    fn test_set_obsidian_vars_complete_workflow() {
1774        let mut config = Ini::new();
1775        config.set(
1776            "obsidian",
1777            "root_path_dir",
1778            Some("~/test/obsidian".to_string()),
1779        );
1780        config.set(
1781            "obsidian",
1782            "commit_path",
1783            Some("~/test/commits".to_string()),
1784        );
1785        config.set(
1786            "templates",
1787            "commit_date_path",
1788            Some("%Y/%m/%d.md".to_string()),
1789        );
1790        config.set(
1791            "templates",
1792            "commit_datetime",
1793            Some("%Y-%m-%d %H:%M:%S".to_string()),
1794        );
1795
1796        let global_vars = GlobalVars::new();
1797        global_vars.config.set(config).unwrap();
1798
1799        // This exercises the full set_obsidian_vars logic
1800        global_vars.set_obsidian_vars();
1801
1802        // Verify all paths were expanded
1803        let root = global_vars.get_obsidian_root_path_dir();
1804        let commit = global_vars.get_obsidian_commit_path();
1805
1806        // Both should have ~ expanded
1807        assert!(!root.to_string_lossy().contains('~'));
1808        assert!(!commit.to_string_lossy().contains('~'));
1809        assert!(root.to_string_lossy().contains("obsidian"));
1810        assert!(commit.to_string_lossy().contains("commits"));
1811    }
1812}
1813
1814#[cfg(test)]
1815mod user_input_tests {
1816    use super::*;
1817    use clap::Parser;
1818
1819    #[test]
1820    fn test_user_input_parse_with_config() {
1821        let args = vec!["test_program", "--config-ini", "/path/to/config.ini"];
1822        let user_input = UserInput::try_parse_from(args).unwrap();
1823
1824        assert_eq!(
1825            user_input.config_ini,
1826            Some("/path/to/config.ini".to_string())
1827        );
1828    }
1829
1830    #[test]
1831    fn test_user_input_parse_without_config() {
1832        let args = vec!["test_program"];
1833        let user_input = UserInput::try_parse_from(args).unwrap();
1834
1835        assert_eq!(user_input.config_ini, None);
1836    }
1837
1838    #[test]
1839    fn test_user_input_parse_short_flag() {
1840        let args = vec!["test_program", "-c", "/short/path/config.ini"];
1841        let user_input = UserInput::try_parse_from(args).unwrap();
1842
1843        assert_eq!(
1844            user_input.config_ini,
1845            Some("/short/path/config.ini".to_string())
1846        );
1847    }
1848
1849    #[test]
1850    fn test_set_proper_home_dir_with_tilde() {
1851        let input = "~/test/path/file.ini";
1852        let result = set_proper_home_dir(input);
1853
1854        // Should replace ~ with actual home directory
1855        assert!(!result.contains('~'));
1856        assert!(result.ends_with("/test/path/file.ini"));
1857    }
1858
1859    #[test]
1860    fn test_set_proper_home_dir_without_tilde() {
1861        let input = "/absolute/path/file.ini";
1862        let result = set_proper_home_dir(input);
1863
1864        // Should remain unchanged
1865        assert_eq!(result, input);
1866    }
1867
1868    #[test]
1869    fn test_set_proper_home_dir_multiple_tildes() {
1870        let input = "~/path/~/file.ini";
1871        let result = set_proper_home_dir(input);
1872
1873        // Should replace ALL tildes
1874        assert!(!result.contains('~'));
1875    }
1876
1877    #[test]
1878    fn test_get_default_ini_path() {
1879        let result = get_default_ini_path();
1880
1881        // Should end with the expected config path
1882        assert!(result.ends_with(".config/rusty-commit-saver/rusty-commit-saver.ini"));
1883
1884        // Should NOT contain literal tilde
1885        assert!(!result.contains('~'));
1886
1887        // Should be an absolute path
1888        assert!(result.starts_with('/'));
1889    }
1890
1891    #[test]
1892    fn test_get_or_default_config_ini_path_with_config_and_tilde() {
1893        // Simulate CLI args: --config-ini ~/my/config.ini
1894        let args = vec!["test", "--config-ini", "~/my/config.ini"];
1895        let user_input = UserInput::try_parse_from(args).unwrap();
1896
1897        // We can't directly call get_or_default_config_ini_path() because it parses env args
1898        // Instead, test that UserInput correctly parses the config path
1899        assert_eq!(user_input.config_ini, Some("~/my/config.ini".to_string()));
1900    }
1901
1902    #[test]
1903    fn test_get_or_default_config_ini_path_with_config_absolute_path() {
1904        // Simulate CLI args: --config-ini /absolute/path/config.ini
1905        let args = vec!["test", "--config-ini", "/absolute/path/config.ini"];
1906        let user_input = UserInput::try_parse_from(args).unwrap();
1907
1908        assert_eq!(
1909            user_input.config_ini,
1910            Some("/absolute/path/config.ini".to_string())
1911        );
1912    }
1913
1914    #[test]
1915    fn test_get_or_default_config_ini_path_without_config() {
1916        // Simulate CLI args with no config specified
1917        let args = vec!["test"];
1918        let user_input = UserInput::try_parse_from(args).unwrap();
1919
1920        // Should default to None, and get_or_default_config_ini_path() will use get_default_ini_path()
1921        assert_eq!(user_input.config_ini, None);
1922    }
1923
1924    #[test]
1925    fn test_parse_ini_content_valid() {
1926        let content = r"
1927[obsidian]
1928root_path_dir=~/Documents/Obsidian
1929commit_path=Diaries/Commits
1930
1931[templates]
1932commit_date_path=%Y/%m-%B/%F.md
1933commit_datetime=%Y-%m-%d
1934";
1935
1936        let result = parse_ini_content(content);
1937        assert!(result.is_ok());
1938
1939        let ini = result.unwrap();
1940        assert_eq!(
1941            ini.get("obsidian", "root_path_dir"),
1942            Some("~/Documents/Obsidian".to_string())
1943        );
1944        assert_eq!(
1945            ini.get("templates", "commit_date_path"),
1946            Some("%Y/%m-%B/%F.md".to_string())
1947        );
1948    }
1949
1950    #[test]
1951    fn test_parse_ini_content_invalid() {
1952        let content = "this is not valid ini format [[[";
1953
1954        let result = parse_ini_content(content);
1955        // Should succeed because configparser is very lenient, but let's verify it doesn't panic
1956        assert!(result.is_ok() || result.is_err());
1957    }
1958
1959    #[test]
1960    fn test_parse_ini_content_empty() {
1961        let content = "";
1962
1963        let result = parse_ini_content(content);
1964        assert!(result.is_ok());
1965
1966        let ini = result.unwrap();
1967        assert_eq!(ini.sections().len(), 0);
1968    }
1969
1970    #[test]
1971    fn test_retrieve_config_file_path_with_temp_file() {
1972        use std::io::Write;
1973        use tempfile::NamedTempFile;
1974
1975        // Create a temporary config file
1976        let mut temp_file = NamedTempFile::new().unwrap();
1977        writeln!(temp_file, "[obsidian]").unwrap();
1978        writeln!(temp_file, "root_path_dir=/tmp/test").unwrap();
1979        writeln!(temp_file, "commit_path=commits").unwrap();
1980        writeln!(temp_file, "[templates]").unwrap();
1981        writeln!(temp_file, "commit_date_path=%Y-%m-%d.md").unwrap();
1982        writeln!(temp_file, "commit_datetime=%Y-%m-%d").unwrap();
1983        temp_file.flush().unwrap();
1984
1985        // Set CLI args to point to our temp file
1986        // We need to simulate CLI args via environment
1987        let path = temp_file.path().to_str().unwrap();
1988
1989        // Instead of testing retrieve_config_file_path directly (which reads from CLI),
1990        // test that we can read and parse a config file
1991        let content = std::fs::read_to_string(path).unwrap();
1992        let result = parse_ini_content(&content);
1993
1994        assert!(result.is_ok());
1995        let ini = result.unwrap();
1996        assert_eq!(
1997            ini.get("obsidian", "root_path_dir"),
1998            Some("/tmp/test".to_string())
1999        );
2000    }
2001
2002    #[test]
2003    fn test_ini_parsing_integration() {
2004        let content = r"
2005[obsidian]
2006root_path_dir=~/Documents/Obsidian
2007commit_path=Diaries/Commits
2008
2009[templates]
2010commit_date_path=%Y/%m-%B/%F.md
2011commit_datetime=%Y-%m-%d %H:%M:%S
2012";
2013
2014        let ini = parse_ini_content(content).unwrap();
2015
2016        // Verify all expected keys exist
2017        assert!(ini.get("obsidian", "root_path_dir").is_some());
2018        assert!(ini.get("obsidian", "commit_path").is_some());
2019        assert!(ini.get("templates", "commit_date_path").is_some());
2020        assert!(ini.get("templates", "commit_datetime").is_some());
2021
2022        // Verify sections count
2023        assert_eq!(ini.sections().len(), 2);
2024    }
2025}