things3_core/
database.rs

1use crate::{
2    error::{Result as ThingsResult, ThingsError},
3    models::{Area, Project, Task, TaskStatus, TaskType},
4};
5use chrono::{DateTime, NaiveDate, Utc};
6use serde::{Deserialize, Serialize};
7use sqlx::{pool::PoolOptions, Row, SqlitePool};
8use std::path::{Path, PathBuf};
9use std::time::Duration;
10use tracing::{debug, error, info, instrument};
11use uuid::Uuid;
12
13/// Convert f64 timestamp to i64 safely
14fn safe_timestamp_convert(ts_f64: f64) -> i64 {
15    // Use try_from to avoid clippy warnings about casting
16    if ts_f64.is_finite() && ts_f64 >= 0.0 {
17        // Use a reasonable upper bound for timestamps (year 2100)
18        let max_timestamp = 4_102_444_800_f64; // 2100-01-01 00:00:00 UTC
19        if ts_f64 <= max_timestamp {
20            // Convert via string to avoid precision loss warnings
21            let ts_str = format!("{:.0}", ts_f64.trunc());
22            ts_str.parse::<i64>().unwrap_or(0)
23        } else {
24            0 // Use epoch if too large
25        }
26    } else {
27        0 // Use epoch if invalid
28    }
29}
30
31/// Convert Things 3 UUID format to standard UUID
32/// Things 3 uses base64-like strings, we'll generate a UUID from the hash
33fn things_uuid_to_uuid(things_uuid: &str) -> Uuid {
34    // For now, create a deterministic UUID from the Things 3 ID
35    // This ensures consistent mapping between Things 3 IDs and UUIDs
36    use std::collections::hash_map::DefaultHasher;
37    use std::hash::{Hash, Hasher};
38
39    let mut hasher = DefaultHasher::new();
40    things_uuid.hash(&mut hasher);
41    let hash = hasher.finish();
42
43    // Create a UUID from the hash (not cryptographically secure, but consistent)
44    // Use proper byte extraction without truncation warnings
45    let bytes = [
46        ((hash >> 56) & 0xFF) as u8,
47        ((hash >> 48) & 0xFF) as u8,
48        ((hash >> 40) & 0xFF) as u8,
49        ((hash >> 32) & 0xFF) as u8,
50        ((hash >> 24) & 0xFF) as u8,
51        ((hash >> 16) & 0xFF) as u8,
52        ((hash >> 8) & 0xFF) as u8,
53        (hash & 0xFF) as u8,
54        // Fill remaining bytes with a pattern based on the string
55        u8::try_from(things_uuid.len().min(255)).unwrap_or(255),
56        things_uuid.chars().next().unwrap_or('0') as u8,
57        things_uuid.chars().nth(1).unwrap_or('0') as u8,
58        things_uuid.chars().nth(2).unwrap_or('0') as u8,
59        things_uuid.chars().nth(3).unwrap_or('0') as u8,
60        things_uuid.chars().nth(4).unwrap_or('0') as u8,
61        things_uuid.chars().nth(5).unwrap_or('0') as u8,
62        things_uuid.chars().nth(6).unwrap_or('0') as u8,
63    ];
64
65    Uuid::from_bytes(bytes)
66}
67
68impl TaskStatus {
69    fn from_i32(value: i32) -> Option<Self> {
70        match value {
71            0 => Some(TaskStatus::Incomplete),
72            1 => Some(TaskStatus::Completed),
73            2 => Some(TaskStatus::Canceled),
74            3 => Some(TaskStatus::Trashed),
75            _ => None,
76        }
77    }
78}
79
80impl TaskType {
81    fn from_i32(value: i32) -> Option<Self> {
82        match value {
83            0 => Some(TaskType::Todo),
84            1 => Some(TaskType::Project),
85            2 => Some(TaskType::Heading),
86            3 => Some(TaskType::Area),
87            _ => None,
88        }
89    }
90}
91
92/// Database connection pool configuration for optimal performance
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct DatabasePoolConfig {
95    /// Maximum number of connections in the pool
96    pub max_connections: u32,
97    /// Minimum number of connections in the pool
98    pub min_connections: u32,
99    /// Connection timeout
100    pub connect_timeout: Duration,
101    /// Idle timeout for connections
102    pub idle_timeout: Duration,
103    /// Maximum lifetime of a connection
104    pub max_lifetime: Duration,
105    /// Test connections before use
106    pub test_before_acquire: bool,
107    /// SQLite-specific optimizations
108    pub sqlite_optimizations: SqliteOptimizations,
109}
110
111/// SQLite-specific optimization settings
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct SqliteOptimizations {
114    /// Enable WAL mode for better concurrency
115    pub enable_wal_mode: bool,
116    /// Set synchronous mode (NORMAL, FULL, OFF)
117    pub synchronous_mode: String,
118    /// Cache size in pages (negative = KB)
119    pub cache_size: i32,
120    /// Enable foreign key constraints
121    pub enable_foreign_keys: bool,
122    /// Set journal mode
123    pub journal_mode: String,
124    /// Set temp store (MEMORY, FILE, DEFAULT)
125    pub temp_store: String,
126    /// Set mmap size for better performance
127    pub mmap_size: i64,
128    /// Enable query planner optimizations
129    pub enable_query_planner: bool,
130}
131
132impl Default for DatabasePoolConfig {
133    fn default() -> Self {
134        Self {
135            max_connections: 10,
136            min_connections: 1,
137            connect_timeout: Duration::from_secs(30),
138            idle_timeout: Duration::from_secs(600), // 10 minutes
139            max_lifetime: Duration::from_secs(1800), // 30 minutes
140            test_before_acquire: true,
141            sqlite_optimizations: SqliteOptimizations::default(),
142        }
143    }
144}
145
146impl Default for SqliteOptimizations {
147    fn default() -> Self {
148        Self {
149            enable_wal_mode: true,
150            synchronous_mode: "NORMAL".to_string(),
151            cache_size: -20000, // 20MB cache
152            enable_foreign_keys: true,
153            journal_mode: "WAL".to_string(),
154            temp_store: "MEMORY".to_string(),
155            mmap_size: 268_435_456, // 256MB
156            enable_query_planner: true,
157        }
158    }
159}
160
161/// Connection pool health status
162#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct PoolHealthStatus {
164    pub is_healthy: bool,
165    pub pool_size: u32,
166    pub active_connections: u32,
167    pub idle_connections: u32,
168    pub max_connections: u32,
169    pub min_connections: u32,
170    pub connection_timeout: Duration,
171    pub idle_timeout: Option<Duration>,
172    pub max_lifetime: Option<Duration>,
173}
174
175/// Detailed connection pool metrics
176#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct PoolMetrics {
178    pub pool_size: u32,
179    pub active_connections: u32,
180    pub idle_connections: u32,
181    pub max_connections: u32,
182    pub min_connections: u32,
183    pub utilization_percentage: f64,
184    pub is_healthy: bool,
185    pub response_time_ms: u64,
186    pub connection_timeout: Duration,
187    pub idle_timeout: Option<Duration>,
188    pub max_lifetime: Option<Duration>,
189}
190
191/// Comprehensive health status including pool and database
192#[derive(Debug, Clone, Serialize, Deserialize)]
193pub struct ComprehensiveHealthStatus {
194    pub overall_healthy: bool,
195    pub pool_health: PoolHealthStatus,
196    pub pool_metrics: PoolMetrics,
197    pub database_stats: DatabaseStats,
198    pub timestamp: DateTime<Utc>,
199}
200
201/// SQLx-based database implementation for Things 3 data
202/// This provides async, Send + Sync compatible database access
203#[derive(Debug, Clone)]
204pub struct ThingsDatabase {
205    pool: SqlitePool,
206    config: DatabasePoolConfig,
207}
208
209impl ThingsDatabase {
210    /// Create a new database connection pool with default configuration
211    ///
212    /// # Errors
213    ///
214    /// Returns an error if the database connection fails or if `SQLite` configuration fails
215    #[instrument]
216    pub async fn new(database_path: &Path) -> ThingsResult<Self> {
217        Self::new_with_config(database_path, DatabasePoolConfig::default()).await
218    }
219
220    /// Create a new database connection pool with custom configuration
221    ///
222    /// # Errors
223    ///
224    /// Returns an error if the database connection fails or if `SQLite` configuration fails
225    #[instrument]
226    pub async fn new_with_config(
227        database_path: &Path,
228        config: DatabasePoolConfig,
229    ) -> ThingsResult<Self> {
230        let database_url = format!("sqlite:{}", database_path.display());
231
232        info!(
233            "Connecting to SQLite database at: {} with optimized pool",
234            database_url
235        );
236
237        // Create optimized connection pool
238        let pool = PoolOptions::new()
239            .max_connections(config.max_connections)
240            .min_connections(config.min_connections)
241            .acquire_timeout(config.connect_timeout)
242            .idle_timeout(Some(config.idle_timeout))
243            .max_lifetime(Some(config.max_lifetime))
244            .test_before_acquire(config.test_before_acquire)
245            .connect(&database_url)
246            .await
247            .map_err(|e| ThingsError::unknown(format!("Failed to connect to database: {e}")))?;
248
249        // Apply SQLite optimizations
250        Self::apply_sqlite_optimizations(&pool, &config.sqlite_optimizations).await?;
251
252        info!(
253            "Database connection pool established successfully with {} max connections",
254            config.max_connections
255        );
256
257        Ok(Self { pool, config })
258    }
259
260    /// Apply SQLite-specific optimizations
261    async fn apply_sqlite_optimizations(
262        pool: &SqlitePool,
263        optimizations: &SqliteOptimizations,
264    ) -> ThingsResult<()> {
265        // Set journal mode
266        sqlx::query(&format!(
267            "PRAGMA journal_mode = {}",
268            optimizations.journal_mode
269        ))
270        .execute(pool)
271        .await
272        .map_err(|e| ThingsError::unknown(format!("Failed to set journal mode: {e}")))?;
273
274        // Set synchronous mode
275        sqlx::query(&format!(
276            "PRAGMA synchronous = {}",
277            optimizations.synchronous_mode
278        ))
279        .execute(pool)
280        .await
281        .map_err(|e| ThingsError::unknown(format!("Failed to set synchronous mode: {e}")))?;
282
283        // Set cache size
284        sqlx::query(&format!("PRAGMA cache_size = {}", optimizations.cache_size))
285            .execute(pool)
286            .await
287            .map_err(|e| ThingsError::unknown(format!("Failed to set cache size: {e}")))?;
288
289        // Set foreign keys
290        let fk_setting = if optimizations.enable_foreign_keys {
291            "ON"
292        } else {
293            "OFF"
294        };
295        sqlx::query(&format!("PRAGMA foreign_keys = {fk_setting}"))
296            .execute(pool)
297            .await
298            .map_err(|e| ThingsError::unknown(format!("Failed to set foreign keys: {e}")))?;
299
300        // Set temp store
301        sqlx::query(&format!("PRAGMA temp_store = {}", optimizations.temp_store))
302            .execute(pool)
303            .await
304            .map_err(|e| ThingsError::unknown(format!("Failed to set temp store: {e}")))?;
305
306        // Set mmap size
307        sqlx::query(&format!("PRAGMA mmap_size = {}", optimizations.mmap_size))
308            .execute(pool)
309            .await
310            .map_err(|e| ThingsError::unknown(format!("Failed to set mmap size: {e}")))?;
311
312        // Enable query planner optimizations
313        if optimizations.enable_query_planner {
314            sqlx::query("PRAGMA optimize")
315                .execute(pool)
316                .await
317                .map_err(|e| ThingsError::unknown(format!("Failed to optimize database: {e}")))?;
318        }
319
320        debug!(
321            "Applied SQLite optimizations: WAL={}, sync={}, cache={}KB, fk={}, temp={}, mmap={}MB",
322            optimizations.enable_wal_mode,
323            optimizations.synchronous_mode,
324            optimizations.cache_size.abs() / 1024,
325            optimizations.enable_foreign_keys,
326            optimizations.temp_store,
327            optimizations.mmap_size / 1024 / 1024
328        );
329
330        Ok(())
331    }
332
333    /// Create a new database connection pool from a connection string with default configuration
334    ///
335    /// # Errors
336    ///
337    /// Returns an error if the database connection fails or if `SQLite` configuration fails
338    #[instrument]
339    pub async fn from_connection_string(database_url: &str) -> ThingsResult<Self> {
340        Self::from_connection_string_with_config(database_url, DatabasePoolConfig::default()).await
341    }
342
343    /// Create a new database connection pool from a connection string with custom configuration
344    ///
345    /// # Errors
346    ///
347    /// Returns an error if the database connection fails or if `SQLite` configuration fails
348    #[instrument]
349    pub async fn from_connection_string_with_config(
350        database_url: &str,
351        config: DatabasePoolConfig,
352    ) -> ThingsResult<Self> {
353        info!(
354            "Connecting to SQLite database: {} with optimized pool",
355            database_url
356        );
357
358        // Create optimized connection pool
359        let pool = PoolOptions::new()
360            .max_connections(config.max_connections)
361            .min_connections(config.min_connections)
362            .acquire_timeout(config.connect_timeout)
363            .idle_timeout(Some(config.idle_timeout))
364            .max_lifetime(Some(config.max_lifetime))
365            .test_before_acquire(config.test_before_acquire)
366            .connect(database_url)
367            .await
368            .map_err(|e| ThingsError::unknown(format!("Failed to connect to database: {e}")))?;
369
370        // Apply SQLite optimizations
371        Self::apply_sqlite_optimizations(&pool, &config.sqlite_optimizations).await?;
372
373        info!(
374            "Database connection pool established successfully with {} max connections",
375            config.max_connections
376        );
377
378        Ok(Self { pool, config })
379    }
380
381    /// Get the underlying connection pool
382    #[must_use]
383    pub fn pool(&self) -> &SqlitePool {
384        &self.pool
385    }
386
387    /// Check if the database is connected
388    #[instrument]
389    pub async fn is_connected(&self) -> bool {
390        match sqlx::query("SELECT 1").fetch_one(&self.pool).await {
391            Ok(_) => {
392                debug!("Database connection is healthy");
393                true
394            }
395            Err(e) => {
396                error!("Database connection check failed: {}", e);
397                false
398            }
399        }
400    }
401
402    /// Get connection pool health status
403    ///
404    /// # Errors
405    ///
406    /// Returns an error if the health check fails
407    #[instrument]
408    pub async fn get_pool_health(&self) -> ThingsResult<PoolHealthStatus> {
409        let pool_size = self.pool.size();
410        let idle_connections = self.pool.num_idle();
411        let active_connections = pool_size - u32::try_from(idle_connections).unwrap_or(0);
412
413        // Test a simple query to verify connection health
414        let is_healthy = self.is_connected().await;
415
416        Ok(PoolHealthStatus {
417            is_healthy,
418            pool_size,
419            active_connections,
420            idle_connections: u32::try_from(idle_connections).unwrap_or(0),
421            max_connections: self.config.max_connections,
422            min_connections: self.config.min_connections,
423            connection_timeout: self.config.connect_timeout,
424            idle_timeout: Some(self.config.idle_timeout),
425            max_lifetime: Some(self.config.max_lifetime),
426        })
427    }
428
429    /// Get detailed connection pool metrics
430    ///
431    /// # Errors
432    ///
433    /// Returns an error if the metrics collection fails
434    #[instrument]
435    pub async fn get_pool_metrics(&self) -> ThingsResult<PoolMetrics> {
436        let pool_size = self.pool.size();
437        let idle_connections = self.pool.num_idle();
438        let active_connections = pool_size - u32::try_from(idle_connections).unwrap_or(0);
439
440        // Calculate utilization percentage
441        let max_connections = self.config.max_connections;
442        let utilization_percentage = if max_connections > 0 {
443            (f64::from(active_connections) / f64::from(max_connections)) * 100.0
444        } else {
445            0.0
446        };
447
448        // Test connection response time
449        let start_time = std::time::Instant::now();
450        let is_connected = self.is_connected().await;
451        let response_time_ms = u64::try_from(start_time.elapsed().as_millis()).unwrap_or(0);
452
453        Ok(PoolMetrics {
454            pool_size,
455            active_connections,
456            idle_connections: u32::try_from(idle_connections).unwrap_or(0),
457            max_connections,
458            min_connections: self.config.min_connections,
459            utilization_percentage,
460            is_healthy: is_connected,
461            response_time_ms,
462            connection_timeout: self.config.connect_timeout,
463            idle_timeout: Some(self.config.idle_timeout),
464            max_lifetime: Some(self.config.max_lifetime),
465        })
466    }
467
468    /// Perform a comprehensive health check including pool and database
469    ///
470    /// # Errors
471    ///
472    /// Returns an error if the health check fails
473    #[instrument]
474    pub async fn comprehensive_health_check(&self) -> ThingsResult<ComprehensiveHealthStatus> {
475        let pool_health = self.get_pool_health().await?;
476        let pool_metrics = self.get_pool_metrics().await?;
477        let db_stats = self.get_stats().await?;
478
479        let overall_healthy = pool_health.is_healthy && pool_metrics.is_healthy;
480
481        Ok(ComprehensiveHealthStatus {
482            overall_healthy,
483            pool_health,
484            pool_metrics,
485            database_stats: db_stats,
486            timestamp: Utc::now(),
487        })
488    }
489
490    /// Get database statistics
491    ///
492    /// # Errors
493    ///
494    /// Returns an error if the database query fails
495    #[instrument]
496    pub async fn get_stats(&self) -> ThingsResult<DatabaseStats> {
497        let task_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM TMTask")
498            .fetch_one(&self.pool)
499            .await
500            .map_err(|e| ThingsError::unknown(format!("Failed to get task count: {e}")))?;
501
502        let project_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM TMTask WHERE type = 1")
503            .fetch_one(&self.pool)
504            .await
505            .map_err(|e| ThingsError::unknown(format!("Failed to get project count: {e}")))?;
506
507        let area_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM TMArea")
508            .fetch_one(&self.pool)
509            .await
510            .map_err(|e| ThingsError::unknown(format!("Failed to get area count: {e}")))?;
511
512        Ok(DatabaseStats {
513            task_count: task_count.try_into().unwrap_or(0),
514            project_count: project_count.try_into().unwrap_or(0),
515            area_count: area_count.try_into().unwrap_or(0),
516        })
517    }
518
519    /// Get all tasks
520    ///
521    /// # Errors
522    ///
523    /// Returns an error if the database query fails or if task data is invalid
524    #[instrument]
525    pub async fn get_all_tasks(&self) -> ThingsResult<Vec<Task>> {
526        let rows = sqlx::query(
527            r"
528            SELECT 
529                uuid, title, status, type, 
530                start_date, due_date, 
531                project_uuid, area_uuid, 
532                notes, tags, 
533                created, modified
534            FROM TMTask
535            ORDER BY created DESC
536            ",
537        )
538        .fetch_all(&self.pool)
539        .await
540        .map_err(|e| ThingsError::unknown(format!("Failed to fetch tasks: {e}")))?;
541
542        let mut tasks = Vec::new();
543        for row in rows {
544            let task = Task {
545                uuid: Uuid::parse_str(&row.get::<String, _>("uuid"))
546                    .map_err(|e| ThingsError::unknown(format!("Invalid task UUID: {e}")))?,
547                title: row.get("title"),
548                status: TaskStatus::from_i32(row.get("status")).unwrap_or(TaskStatus::Incomplete),
549                task_type: TaskType::from_i32(row.get("type")).unwrap_or(TaskType::Todo),
550                start_date: row
551                    .get::<Option<String>, _>("start_date")
552                    .and_then(|s| NaiveDate::parse_from_str(&s, "%Y-%m-%d").ok()),
553                deadline: row
554                    .get::<Option<String>, _>("due_date")
555                    .and_then(|s| NaiveDate::parse_from_str(&s, "%Y-%m-%d").ok()),
556                project_uuid: row
557                    .get::<Option<String>, _>("project_uuid")
558                    .and_then(|s| Uuid::parse_str(&s).ok()),
559                area_uuid: row
560                    .get::<Option<String>, _>("area_uuid")
561                    .and_then(|s| Uuid::parse_str(&s).ok()),
562                parent_uuid: None, // Not available in this query
563                notes: row.get("notes"),
564                tags: row
565                    .get::<Option<String>, _>("tags")
566                    .map(|s| s.split(',').map(|s| s.trim().to_string()).collect())
567                    .unwrap_or_default(),
568                children: Vec::new(), // Not available in this query
569                created: DateTime::parse_from_rfc3339(&row.get::<String, _>("created"))
570                    .ok()
571                    .map_or_else(Utc::now, |dt| dt.with_timezone(&Utc)),
572                modified: DateTime::parse_from_rfc3339(&row.get::<String, _>("modified"))
573                    .ok()
574                    .map_or_else(Utc::now, |dt| dt.with_timezone(&Utc)),
575            };
576            tasks.push(task);
577        }
578
579        debug!("Fetched {} tasks", tasks.len());
580        Ok(tasks)
581    }
582
583    /// Get all projects (from `TMTask` table where type = 1)
584    ///
585    /// # Errors
586    ///
587    /// Returns an error if the database query fails or if project data is invalid
588    #[instrument]
589    pub async fn get_all_projects(&self) -> ThingsResult<Vec<Project>> {
590        let rows = sqlx::query(
591            r"
592            SELECT 
593                uuid, title, status, 
594                area, notes, 
595                creationDate, userModificationDate,
596                startDate, deadline
597            FROM TMTask
598            WHERE type = 1 AND trashed = 0
599            ORDER BY creationDate DESC
600            ",
601        )
602        .fetch_all(&self.pool)
603        .await
604        .map_err(|e| ThingsError::unknown(format!("Failed to fetch projects: {e}")))?;
605
606        let mut projects = Vec::new();
607        for row in rows {
608            let project = Project {
609                uuid: things_uuid_to_uuid(&row.get::<String, _>("uuid")),
610                title: row.get("title"),
611                status: TaskStatus::from_i32(row.get("status")).unwrap_or(TaskStatus::Incomplete),
612                area_uuid: row
613                    .get::<Option<String>, _>("area")
614                    .map(|s| things_uuid_to_uuid(&s)),
615                notes: row.get("notes"),
616                deadline: row
617                    .get::<Option<i64>, _>("deadline")
618                    .and_then(|ts| DateTime::from_timestamp(ts, 0))
619                    .map(|dt| dt.date_naive()),
620                start_date: row
621                    .get::<Option<i64>, _>("startDate")
622                    .and_then(|ts| DateTime::from_timestamp(ts, 0))
623                    .map(|dt| dt.date_naive()),
624                tags: Vec::new(),  // TODO: Load tags separately
625                tasks: Vec::new(), // TODO: Load child tasks separately
626                created: {
627                    let ts_f64 = row.get::<f64, _>("creationDate");
628                    let ts = safe_timestamp_convert(ts_f64);
629                    DateTime::from_timestamp(ts, 0).unwrap_or_else(Utc::now)
630                },
631                modified: {
632                    let ts_f64 = row.get::<f64, _>("userModificationDate");
633                    let ts = safe_timestamp_convert(ts_f64);
634                    DateTime::from_timestamp(ts, 0).unwrap_or_else(Utc::now)
635                },
636            };
637            projects.push(project);
638        }
639
640        debug!("Fetched {} projects", projects.len());
641        Ok(projects)
642    }
643
644    /// Get all areas
645    ///
646    /// # Errors
647    ///
648    /// Returns an error if the database query fails or if area data is invalid
649    #[instrument]
650    pub async fn get_all_areas(&self) -> ThingsResult<Vec<Area>> {
651        let rows = sqlx::query(
652            r"
653            SELECT 
654                uuid, title, visible, `index`
655             FROM TMArea 
656             WHERE visible = 1
657            ORDER BY `index` ASC
658            ",
659        )
660        .fetch_all(&self.pool)
661        .await
662        .map_err(|e| ThingsError::unknown(format!("Failed to fetch areas: {e}")))?;
663
664        let mut areas = Vec::new();
665        for row in rows {
666            let area = Area {
667                uuid: things_uuid_to_uuid(&row.get::<String, _>("uuid")),
668                title: row.get("title"),
669                notes: None,          // Notes not stored in TMArea table
670                projects: Vec::new(), // TODO: Load projects separately
671                tags: Vec::new(),     // TODO: Load tags separately
672                created: Utc::now(),  // Creation date not available in TMArea
673                modified: Utc::now(), // Modification date not available in TMArea
674            };
675            areas.push(area);
676        }
677
678        debug!("Fetched {} areas", areas.len());
679        Ok(areas)
680    }
681
682    /// Get tasks by status
683    ///
684    /// # Errors
685    ///
686    /// Returns an error if the database query fails or if task data is invalid
687    #[instrument]
688    pub async fn get_tasks_by_status(&self, status: TaskStatus) -> ThingsResult<Vec<Task>> {
689        let status_value = status as i32;
690        let rows = sqlx::query(
691            r"
692            SELECT 
693                uuid, title, status, type, 
694                start_date, due_date, 
695                project_uuid, area_uuid, 
696                notes, tags, 
697                created, modified
698             FROM TMTask 
699            WHERE status = ?
700            ORDER BY created DESC
701            ",
702        )
703        .bind(status_value)
704        .fetch_all(&self.pool)
705        .await
706        .map_err(|e| ThingsError::unknown(format!("Failed to fetch tasks by status: {e}")))?;
707
708        let mut tasks = Vec::new();
709        for row in rows {
710            let task = Task {
711                uuid: Uuid::parse_str(&row.get::<String, _>("uuid"))
712                    .map_err(|e| ThingsError::unknown(format!("Invalid task UUID: {e}")))?,
713                title: row.get("title"),
714                status: TaskStatus::from_i32(row.get("status")).unwrap_or(TaskStatus::Incomplete),
715                task_type: TaskType::from_i32(row.get("type")).unwrap_or(TaskType::Todo),
716                start_date: row
717                    .get::<Option<String>, _>("start_date")
718                    .and_then(|s| NaiveDate::parse_from_str(&s, "%Y-%m-%d").ok()),
719                deadline: row
720                    .get::<Option<String>, _>("due_date")
721                    .and_then(|s| NaiveDate::parse_from_str(&s, "%Y-%m-%d").ok()),
722                project_uuid: row
723                    .get::<Option<String>, _>("project_uuid")
724                    .and_then(|s| Uuid::parse_str(&s).ok()),
725                area_uuid: row
726                    .get::<Option<String>, _>("area_uuid")
727                    .and_then(|s| Uuid::parse_str(&s).ok()),
728                parent_uuid: None, // Not available in this query
729                notes: row.get("notes"),
730                tags: row
731                    .get::<Option<String>, _>("tags")
732                    .map(|s| s.split(',').map(|s| s.trim().to_string()).collect())
733                    .unwrap_or_default(),
734                children: Vec::new(), // Not available in this query
735                created: DateTime::parse_from_rfc3339(&row.get::<String, _>("created"))
736                    .ok()
737                    .map_or_else(Utc::now, |dt| dt.with_timezone(&Utc)),
738                modified: DateTime::parse_from_rfc3339(&row.get::<String, _>("modified"))
739                    .ok()
740                    .map_or_else(Utc::now, |dt| dt.with_timezone(&Utc)),
741            };
742            tasks.push(task);
743        }
744
745        debug!("Fetched {} tasks with status {:?}", tasks.len(), status);
746        Ok(tasks)
747    }
748
749    /// Search tasks by title or notes
750    ///
751    /// # Errors
752    ///
753    /// Returns an error if the database query fails or if task data is invalid
754    #[instrument]
755    pub async fn search_tasks(&self, query: &str) -> ThingsResult<Vec<Task>> {
756        let search_pattern = format!("%{query}%");
757        let rows = sqlx::query(
758            r"
759            SELECT 
760                uuid, title, status, type, 
761                startDate, deadline, 
762                project, area,
763                notes, cachedTags, 
764                creationDate, userModificationDate
765            FROM TMTask
766            WHERE (title LIKE ? OR notes LIKE ?) AND trashed = 0 AND type = 0
767            ORDER BY creationDate DESC
768            ",
769        )
770        .bind(&search_pattern)
771        .bind(&search_pattern)
772        .fetch_all(&self.pool)
773        .await
774        .map_err(|e| ThingsError::unknown(format!("Failed to search tasks: {e}")))?;
775
776        let mut tasks = Vec::new();
777        for row in rows {
778            let task = Task {
779                uuid: things_uuid_to_uuid(&row.get::<String, _>("uuid")),
780                title: row.get("title"),
781                status: TaskStatus::from_i32(row.get("status")).unwrap_or(TaskStatus::Incomplete),
782                task_type: TaskType::from_i32(row.get("type")).unwrap_or(TaskType::Todo),
783                start_date: row
784                    .get::<Option<i64>, _>("startDate")
785                    .and_then(|ts| DateTime::from_timestamp(ts, 0))
786                    .map(|dt| dt.date_naive()),
787                deadline: row
788                    .get::<Option<i64>, _>("deadline")
789                    .and_then(|ts| DateTime::from_timestamp(ts, 0))
790                    .map(|dt| dt.date_naive()),
791                project_uuid: row
792                    .get::<Option<String>, _>("project")
793                    .map(|s| things_uuid_to_uuid(&s)),
794                area_uuid: row
795                    .get::<Option<String>, _>("area")
796                    .map(|s| things_uuid_to_uuid(&s)),
797                parent_uuid: None, // No parent column in real schema
798                notes: row.get("notes"),
799                tags: row
800                    .get::<Option<Vec<u8>>, _>("cachedTags")
801                    .map(|_| Vec::new()) // TODO: Parse binary tag data
802                    .unwrap_or_default(),
803                children: Vec::new(), // Not available in this query
804                created: {
805                    let ts_f64 = row.get::<f64, _>("creationDate");
806                    let ts = safe_timestamp_convert(ts_f64);
807                    DateTime::from_timestamp(ts, 0).unwrap_or_else(Utc::now)
808                },
809                modified: {
810                    let ts_f64 = row.get::<f64, _>("userModificationDate");
811                    let ts = safe_timestamp_convert(ts_f64);
812                    DateTime::from_timestamp(ts, 0).unwrap_or_else(Utc::now)
813                },
814            };
815            tasks.push(task);
816        }
817
818        debug!("Found {} tasks matching query: {}", tasks.len(), query);
819        Ok(tasks)
820    }
821
822    /// Get inbox tasks (incomplete tasks without project)
823    ///
824    /// # Errors
825    ///
826    /// Returns an error if the database query fails or if task data is invalid
827    #[instrument(skip(self))]
828    pub async fn get_inbox(&self, limit: Option<usize>) -> ThingsResult<Vec<Task>> {
829        let query = if let Some(limit) = limit {
830            format!("SELECT uuid, title, type, status, notes, startDate, deadline, creationDate, userModificationDate, project, area, parent, tags FROM TMTask WHERE type = 0 AND status = 0 AND project IS NULL AND trashed = 0 ORDER BY creationDate DESC LIMIT {limit}")
831        } else {
832            "SELECT uuid, title, type, status, notes, startDate, deadline, creationDate, userModificationDate, project, area, parent, tags FROM TMTask WHERE type = 0 AND status = 0 AND project IS NULL AND trashed = 0 ORDER BY creationDate DESC"
833                .to_string()
834        };
835
836        let rows = sqlx::query(&query)
837            .fetch_all(&self.pool)
838            .await
839            .map_err(|e| ThingsError::unknown(format!("Failed to fetch inbox tasks: {e}")))?;
840
841        let tasks = rows
842            .into_iter()
843            .map(|row| {
844                Ok(Task {
845                    uuid: things_uuid_to_uuid(&row.get::<String, _>("uuid")),
846                    title: row.get("title"),
847                    task_type: TaskType::from_i32(row.get("type")).unwrap_or(TaskType::Todo),
848                    status: TaskStatus::from_i32(row.get("status"))
849                        .unwrap_or(TaskStatus::Incomplete),
850                    notes: row.get("notes"),
851                    start_date: row
852                        .get::<Option<i64>, _>("startDate")
853                        .and_then(|ts| DateTime::from_timestamp(ts, 0))
854                        .map(|dt| dt.date_naive()),
855                    deadline: row
856                        .get::<Option<i64>, _>("deadline")
857                        .and_then(|ts| DateTime::from_timestamp(ts, 0))
858                        .map(|dt| dt.date_naive()),
859                    created: {
860                        let ts_f64 = row.get::<f64, _>("creationDate");
861                        let ts = safe_timestamp_convert(ts_f64);
862                        DateTime::from_timestamp(ts, 0).unwrap_or_else(Utc::now)
863                    },
864                    modified: {
865                        let ts_f64 = row.get::<f64, _>("userModificationDate");
866                        let ts = safe_timestamp_convert(ts_f64);
867                        DateTime::from_timestamp(ts, 0).unwrap_or_else(Utc::now)
868                    },
869                    project_uuid: row
870                        .get::<Option<String>, _>("project")
871                        .map(|s| things_uuid_to_uuid(&s)),
872                    area_uuid: row
873                        .get::<Option<String>, _>("area")
874                        .map(|s| things_uuid_to_uuid(&s)),
875                    parent_uuid: row
876                        .get::<Option<String>, _>("parent")
877                        .map(|s| things_uuid_to_uuid(&s)),
878                    tags: row
879                        .get::<Option<String>, _>("tags")
880                        .map(|s| s.split(',').map(|s| s.trim().to_string()).collect())
881                        .unwrap_or_default(),
882                    children: Vec::new(),
883                })
884            })
885            .collect::<ThingsResult<Vec<Task>>>()?;
886
887        Ok(tasks)
888    }
889
890    /// Get today's tasks (incomplete tasks due today or started today)
891    ///
892    /// # Errors
893    ///
894    /// Returns an error if the database query fails or if task data is invalid
895    ///
896    /// # Panics
897    ///
898    /// Panics if the current date cannot be converted to a valid time with hours, minutes, and seconds
899    #[instrument(skip(self))]
900    pub async fn get_today(&self, limit: Option<usize>) -> ThingsResult<Vec<Task>> {
901        let today = chrono::Utc::now().date_naive();
902        let start_of_day = today.and_hms_opt(0, 0, 0).unwrap().and_utc().timestamp();
903        let end_of_day = today.and_hms_opt(23, 59, 59).unwrap().and_utc().timestamp();
904
905        let query = if let Some(limit) = limit {
906            format!(
907                "SELECT uuid, title, type, status, notes, startDate, deadline, creationDate, userModificationDate, project, area, parent, tags FROM TMTask WHERE status = 0 AND ((deadline >= ? AND deadline <= ?) OR (startDate >= ? AND startDate <= ?)) AND trashed = 0 ORDER BY creationDate DESC LIMIT {limit}"
908            )
909        } else {
910            "SELECT uuid, title, type, status, notes, startDate, deadline, creationDate, userModificationDate, project, area, parent, tags FROM TMTask WHERE status = 0 AND ((deadline >= ? AND deadline <= ?) OR (startDate >= ? AND startDate <= ?)) AND trashed = 0 ORDER BY creationDate DESC".to_string()
911        };
912
913        let rows = sqlx::query(&query)
914            .bind(start_of_day)
915            .bind(end_of_day)
916            .bind(start_of_day)
917            .bind(end_of_day)
918            .fetch_all(&self.pool)
919            .await
920            .map_err(|e| ThingsError::unknown(format!("Failed to fetch today's tasks: {e}")))?;
921
922        let tasks = rows
923            .into_iter()
924            .map(|row| {
925                Ok(Task {
926                    uuid: things_uuid_to_uuid(&row.get::<String, _>("uuid")),
927                    title: row.get("title"),
928                    task_type: TaskType::from_i32(row.get("type")).unwrap_or(TaskType::Todo),
929                    status: TaskStatus::from_i32(row.get("status"))
930                        .unwrap_or(TaskStatus::Incomplete),
931                    notes: row.get("notes"),
932                    start_date: row
933                        .get::<Option<i64>, _>("startDate")
934                        .and_then(|ts| DateTime::from_timestamp(ts, 0))
935                        .map(|dt| dt.date_naive()),
936                    deadline: row
937                        .get::<Option<i64>, _>("deadline")
938                        .and_then(|ts| DateTime::from_timestamp(ts, 0))
939                        .map(|dt| dt.date_naive()),
940                    created: {
941                        let ts_f64 = row.get::<f64, _>("creationDate");
942                        let ts = safe_timestamp_convert(ts_f64);
943                        DateTime::from_timestamp(ts, 0).unwrap_or_else(Utc::now)
944                    },
945                    modified: {
946                        let ts_f64 = row.get::<f64, _>("userModificationDate");
947                        let ts = safe_timestamp_convert(ts_f64);
948                        DateTime::from_timestamp(ts, 0).unwrap_or_else(Utc::now)
949                    },
950                    project_uuid: row
951                        .get::<Option<String>, _>("project")
952                        .map(|s| things_uuid_to_uuid(&s)),
953                    area_uuid: row
954                        .get::<Option<String>, _>("area")
955                        .map(|s| things_uuid_to_uuid(&s)),
956                    parent_uuid: row
957                        .get::<Option<String>, _>("parent")
958                        .map(|s| things_uuid_to_uuid(&s)),
959                    tags: row
960                        .get::<Option<String>, _>("tags")
961                        .map(|s| s.split(',').map(|s| s.trim().to_string()).collect())
962                        .unwrap_or_default(),
963                    children: Vec::new(),
964                })
965            })
966            .collect::<ThingsResult<Vec<Task>>>()?;
967
968        Ok(tasks)
969    }
970
971    /// Get all projects (alias for `get_all_projects` for compatibility)
972    ///
973    /// # Errors
974    ///
975    /// Returns an error if the database query fails or if project data is invalid
976    #[instrument(skip(self))]
977    pub async fn get_projects(&self, limit: Option<usize>) -> ThingsResult<Vec<Project>> {
978        let _ = limit; // Currently unused but kept for API compatibility
979        self.get_all_projects().await
980    }
981
982    /// Get all areas (alias for `get_all_areas` for compatibility)
983    ///
984    /// # Errors
985    ///
986    /// Returns an error if the database query fails or if area data is invalid
987    #[instrument(skip(self))]
988    pub async fn get_areas(&self) -> ThingsResult<Vec<Area>> {
989        self.get_all_areas().await
990    }
991}
992
993/// Database statistics
994#[derive(Debug, Clone, Serialize, Deserialize)]
995pub struct DatabaseStats {
996    pub task_count: u64,
997    pub project_count: u64,
998    pub area_count: u64,
999}
1000
1001impl DatabaseStats {
1002    #[must_use]
1003    pub fn total_items(&self) -> u64 {
1004        self.task_count + self.project_count + self.area_count
1005    }
1006}
1007
1008/// Get the default Things 3 database path
1009///
1010/// # Examples
1011///
1012/// ```
1013/// use things3_core::get_default_database_path;
1014///
1015/// let path = get_default_database_path();
1016/// assert!(!path.to_string_lossy().is_empty());
1017/// assert!(path.to_string_lossy().contains("Library"));
1018/// ```
1019#[must_use]
1020pub fn get_default_database_path() -> PathBuf {
1021    let home = std::env::var("HOME").unwrap_or_else(|_| "~".to_string());
1022    PathBuf::from(format!(
1023        "{home}/Library/Group Containers/JLMPQHK86H.com.culturedcode.ThingsMac/ThingsData-0Z0Z2/Things Database.thingsdatabase/main.sqlite"
1024    ))
1025}
1026
1027#[cfg(test)]
1028mod tests {
1029    use super::*;
1030    use tempfile::{NamedTempFile, TempDir};
1031
1032    #[tokio::test]
1033    async fn test_database_connection() {
1034        let temp_dir = TempDir::new().unwrap();
1035        let db_path = temp_dir.path().join("test.db");
1036
1037        // This will fail because the database doesn't exist yet
1038        // In a real implementation, we'd need to create the schema first
1039        let result = super::ThingsDatabase::new(&db_path).await;
1040        assert!(result.is_err());
1041    }
1042
1043    #[tokio::test]
1044    async fn test_connection_string() {
1045        let result = super::ThingsDatabase::from_connection_string("sqlite::memory:").await;
1046        assert!(result.is_ok());
1047    }
1048
1049    #[test]
1050    fn test_task_status_from_i32() {
1051        assert_eq!(TaskStatus::from_i32(0), Some(TaskStatus::Incomplete));
1052        assert_eq!(TaskStatus::from_i32(1), Some(TaskStatus::Completed));
1053        assert_eq!(TaskStatus::from_i32(2), Some(TaskStatus::Canceled));
1054        assert_eq!(TaskStatus::from_i32(3), Some(TaskStatus::Trashed));
1055        assert_eq!(TaskStatus::from_i32(4), None);
1056        assert_eq!(TaskStatus::from_i32(-1), None);
1057    }
1058
1059    #[test]
1060    fn test_task_type_from_i32() {
1061        assert_eq!(TaskType::from_i32(0), Some(TaskType::Todo));
1062        assert_eq!(TaskType::from_i32(1), Some(TaskType::Project));
1063        assert_eq!(TaskType::from_i32(2), Some(TaskType::Heading));
1064        assert_eq!(TaskType::from_i32(3), Some(TaskType::Area));
1065        assert_eq!(TaskType::from_i32(4), None);
1066        assert_eq!(TaskType::from_i32(-1), None);
1067    }
1068
1069    #[test]
1070    fn test_database_stats_total_items() {
1071        let stats = DatabaseStats {
1072            task_count: 10,
1073            project_count: 5,
1074            area_count: 3,
1075        };
1076        assert_eq!(stats.total_items(), 18);
1077
1078        let empty_stats = DatabaseStats {
1079            task_count: 0,
1080            project_count: 0,
1081            area_count: 0,
1082        };
1083        assert_eq!(empty_stats.total_items(), 0);
1084    }
1085
1086    #[test]
1087    fn test_database_pool_config_default() {
1088        let config = DatabasePoolConfig::default();
1089        assert_eq!(config.max_connections, 10);
1090        assert_eq!(config.min_connections, 1);
1091        assert_eq!(config.connect_timeout, Duration::from_secs(30));
1092        assert_eq!(config.idle_timeout, Duration::from_secs(600));
1093        assert_eq!(config.max_lifetime, Duration::from_secs(1800));
1094        assert!(config.test_before_acquire);
1095    }
1096
1097    #[test]
1098    fn test_sqlite_optimizations_default() {
1099        let opts = SqliteOptimizations::default();
1100        assert!(opts.enable_wal_mode);
1101        assert_eq!(opts.cache_size, -20000);
1102        assert_eq!(opts.synchronous_mode, "NORMAL".to_string());
1103        assert_eq!(opts.temp_store, "MEMORY".to_string());
1104        assert_eq!(opts.journal_mode, "WAL".to_string());
1105        assert_eq!(opts.mmap_size, 268_435_456);
1106        assert!(opts.enable_foreign_keys);
1107        assert!(opts.enable_query_planner);
1108    }
1109
1110    #[test]
1111    fn test_pool_health_status_creation() {
1112        let status = PoolHealthStatus {
1113            is_healthy: true,
1114            pool_size: 8,
1115            active_connections: 5,
1116            idle_connections: 3,
1117            max_connections: 10,
1118            min_connections: 1,
1119            connection_timeout: Duration::from_secs(30),
1120            idle_timeout: Some(Duration::from_secs(600)),
1121            max_lifetime: Some(Duration::from_secs(1800)),
1122        };
1123        assert!(status.is_healthy);
1124        assert_eq!(status.active_connections, 5);
1125        assert_eq!(status.idle_connections, 3);
1126        assert_eq!(status.pool_size, 8);
1127    }
1128
1129    #[test]
1130    fn test_pool_metrics_creation() {
1131        let metrics = PoolMetrics {
1132            pool_size: 8,
1133            active_connections: 5,
1134            idle_connections: 3,
1135            max_connections: 10,
1136            min_connections: 1,
1137            utilization_percentage: 80.0,
1138            is_healthy: true,
1139            response_time_ms: 50,
1140            connection_timeout: Duration::from_secs(30),
1141            idle_timeout: Some(Duration::from_secs(600)),
1142            max_lifetime: Some(Duration::from_secs(1800)),
1143        };
1144        assert!(metrics.is_healthy);
1145        assert_eq!(metrics.pool_size, 8);
1146        assert_eq!(metrics.active_connections, 5);
1147        assert_eq!(metrics.idle_connections, 3);
1148        assert!((metrics.utilization_percentage - 80.0).abs() < f64::EPSILON);
1149        assert_eq!(metrics.response_time_ms, 50);
1150    }
1151
1152    #[test]
1153    fn test_comprehensive_health_status_creation() {
1154        let pool_health = PoolHealthStatus {
1155            is_healthy: true,
1156            pool_size: 8,
1157            active_connections: 5,
1158            idle_connections: 3,
1159            max_connections: 10,
1160            min_connections: 1,
1161            connection_timeout: Duration::from_secs(30),
1162            idle_timeout: Some(Duration::from_secs(600)),
1163            max_lifetime: Some(Duration::from_secs(1800)),
1164        };
1165
1166        let pool_metrics = PoolMetrics {
1167            pool_size: 8,
1168            active_connections: 5,
1169            idle_connections: 3,
1170            max_connections: 10,
1171            min_connections: 1,
1172            utilization_percentage: 80.0,
1173            is_healthy: true,
1174            response_time_ms: 50,
1175            connection_timeout: Duration::from_secs(30),
1176            idle_timeout: Some(Duration::from_secs(600)),
1177            max_lifetime: Some(Duration::from_secs(1800)),
1178        };
1179
1180        let db_stats = DatabaseStats {
1181            task_count: 50,
1182            project_count: 10,
1183            area_count: 5,
1184        };
1185
1186        let health_status = ComprehensiveHealthStatus {
1187            overall_healthy: true,
1188            pool_health,
1189            pool_metrics,
1190            database_stats: db_stats,
1191            timestamp: Utc::now(),
1192        };
1193
1194        assert!(health_status.overall_healthy);
1195        assert_eq!(health_status.database_stats.total_items(), 65);
1196    }
1197
1198    #[test]
1199    fn test_safe_timestamp_convert_edge_cases() {
1200        // Test normal timestamp
1201        assert_eq!(safe_timestamp_convert(1_609_459_200.0), 1_609_459_200); // 2021-01-01
1202
1203        // Test zero
1204        assert_eq!(safe_timestamp_convert(0.0), 0);
1205
1206        // Test negative (should return 0)
1207        assert_eq!(safe_timestamp_convert(-1.0), 0);
1208
1209        // Test infinity (should return 0)
1210        assert_eq!(safe_timestamp_convert(f64::INFINITY), 0);
1211
1212        // Test NaN (should return 0)
1213        assert_eq!(safe_timestamp_convert(f64::NAN), 0);
1214
1215        // Test very large timestamp (should return 0)
1216        assert_eq!(safe_timestamp_convert(5_000_000_000.0), 0);
1217
1218        // Test max valid timestamp
1219        let max_timestamp = 4_102_444_800_f64; // 2100-01-01
1220        assert_eq!(safe_timestamp_convert(max_timestamp), 4_102_444_800);
1221    }
1222
1223    #[test]
1224    fn test_things_uuid_to_uuid_consistency() {
1225        // Test consistent UUID generation
1226        let things_id = "test-id-123";
1227        let uuid1 = things_uuid_to_uuid(things_id);
1228        let uuid2 = things_uuid_to_uuid(things_id);
1229        assert_eq!(uuid1, uuid2, "UUIDs should be consistent for same input");
1230
1231        // Test different inputs produce different UUIDs
1232        let uuid3 = things_uuid_to_uuid("different-id");
1233        assert_ne!(
1234            uuid1, uuid3,
1235            "Different inputs should produce different UUIDs"
1236        );
1237
1238        // Test empty string
1239        let uuid_empty = things_uuid_to_uuid("");
1240        assert!(!uuid_empty.to_string().is_empty());
1241
1242        // Test very long string
1243        let long_string = "a".repeat(1000);
1244        let uuid_long = things_uuid_to_uuid(&long_string);
1245        assert!(!uuid_long.to_string().is_empty());
1246    }
1247
1248    #[test]
1249    fn test_task_status_from_i32_all_variants() {
1250        assert_eq!(TaskStatus::from_i32(0), Some(TaskStatus::Incomplete));
1251        assert_eq!(TaskStatus::from_i32(1), Some(TaskStatus::Completed));
1252        assert_eq!(TaskStatus::from_i32(2), Some(TaskStatus::Canceled));
1253        assert_eq!(TaskStatus::from_i32(3), Some(TaskStatus::Trashed));
1254        assert_eq!(TaskStatus::from_i32(999), None);
1255        assert_eq!(TaskStatus::from_i32(-1), None);
1256    }
1257
1258    #[test]
1259    fn test_task_type_from_i32_all_variants() {
1260        assert_eq!(TaskType::from_i32(0), Some(TaskType::Todo));
1261        assert_eq!(TaskType::from_i32(1), Some(TaskType::Project));
1262        assert_eq!(TaskType::from_i32(2), Some(TaskType::Heading));
1263        assert_eq!(TaskType::from_i32(3), Some(TaskType::Area));
1264        assert_eq!(TaskType::from_i32(999), None);
1265        assert_eq!(TaskType::from_i32(-1), None);
1266    }
1267
1268    #[test]
1269    fn test_database_pool_config_default_values() {
1270        let config = DatabasePoolConfig::default();
1271        assert_eq!(config.max_connections, 10);
1272        assert_eq!(config.min_connections, 1);
1273        assert_eq!(config.connect_timeout, Duration::from_secs(30));
1274        assert_eq!(config.idle_timeout, Duration::from_secs(600));
1275        assert_eq!(config.max_lifetime, Duration::from_secs(1800));
1276        assert!(config.test_before_acquire);
1277    }
1278
1279    #[test]
1280    fn test_database_stats_total_items_calculation() {
1281        let stats = DatabaseStats {
1282            task_count: 10,
1283            project_count: 5,
1284            area_count: 3,
1285        };
1286        assert_eq!(stats.total_items(), 18); // 10 + 5 + 3
1287
1288        // Test with zero values
1289        let empty_stats = DatabaseStats {
1290            task_count: 0,
1291            project_count: 0,
1292            area_count: 0,
1293        };
1294        assert_eq!(empty_stats.total_items(), 0);
1295    }
1296
1297    #[test]
1298    fn test_pool_health_status_creation_comprehensive() {
1299        let status = PoolHealthStatus {
1300            is_healthy: true,
1301            pool_size: 8,
1302            active_connections: 2,
1303            idle_connections: 3,
1304            max_connections: 10,
1305            min_connections: 1,
1306            connection_timeout: Duration::from_secs(30),
1307            idle_timeout: Some(Duration::from_secs(600)),
1308            max_lifetime: Some(Duration::from_secs(1800)),
1309        };
1310        assert!(status.is_healthy);
1311        assert_eq!(status.pool_size, 8);
1312        assert_eq!(status.max_connections, 10);
1313    }
1314
1315    #[test]
1316    fn test_pool_metrics_creation_comprehensive() {
1317        let metrics = PoolMetrics {
1318            pool_size: 8,
1319            active_connections: 5,
1320            idle_connections: 3,
1321            max_connections: 10,
1322            min_connections: 1,
1323            utilization_percentage: 80.0,
1324            is_healthy: true,
1325            response_time_ms: 50,
1326            connection_timeout: Duration::from_secs(30),
1327            idle_timeout: Some(Duration::from_secs(600)),
1328            max_lifetime: Some(Duration::from_secs(1800)),
1329        };
1330        assert_eq!(metrics.pool_size, 8);
1331        assert_eq!(metrics.response_time_ms, 50);
1332        assert!(metrics.is_healthy);
1333    }
1334
1335    #[test]
1336    fn test_comprehensive_health_status_creation_full() {
1337        let pool_health = PoolHealthStatus {
1338            is_healthy: true,
1339            pool_size: 8,
1340            active_connections: 2,
1341            idle_connections: 3,
1342            max_connections: 10,
1343            min_connections: 1,
1344            connection_timeout: Duration::from_secs(30),
1345            idle_timeout: Some(Duration::from_secs(600)),
1346            max_lifetime: Some(Duration::from_secs(1800)),
1347        };
1348
1349        let pool_metrics = PoolMetrics {
1350            pool_size: 8,
1351            active_connections: 5,
1352            idle_connections: 3,
1353            max_connections: 10,
1354            min_connections: 1,
1355            utilization_percentage: 80.0,
1356            is_healthy: true,
1357            response_time_ms: 50,
1358            connection_timeout: Duration::from_secs(30),
1359            idle_timeout: Some(Duration::from_secs(600)),
1360            max_lifetime: Some(Duration::from_secs(1800)),
1361        };
1362
1363        let database_stats = DatabaseStats {
1364            task_count: 100,
1365            project_count: 20,
1366            area_count: 5,
1367        };
1368
1369        let status = ComprehensiveHealthStatus {
1370            overall_healthy: true,
1371            pool_health,
1372            pool_metrics,
1373            database_stats,
1374            timestamp: Utc::now(),
1375        };
1376
1377        assert!(status.overall_healthy);
1378        assert_eq!(status.database_stats.total_items(), 125);
1379    }
1380
1381    #[test]
1382    fn test_sqlite_optimizations_default_values() {
1383        let opts = SqliteOptimizations::default();
1384        assert!(opts.enable_wal_mode);
1385        assert!(opts.enable_foreign_keys);
1386        assert_eq!(opts.cache_size, -20000);
1387        assert_eq!(opts.temp_store, "MEMORY");
1388        assert_eq!(opts.mmap_size, 268_435_456);
1389        assert_eq!(opts.synchronous_mode, "NORMAL");
1390        assert_eq!(opts.journal_mode, "WAL");
1391    }
1392
1393    #[test]
1394    fn test_get_default_database_path_format() {
1395        let path = get_default_database_path();
1396        let path_str = path.to_string_lossy();
1397        assert!(path_str.contains("Things Database.thingsdatabase"));
1398        assert!(path_str.contains("main.sqlite"));
1399        assert!(path_str.contains("Library/Group Containers"));
1400    }
1401
1402    #[tokio::test]
1403    async fn test_database_new_with_config() {
1404        let temp_file = NamedTempFile::new().unwrap();
1405        let db_path = temp_file.path();
1406
1407        crate::test_utils::create_test_database(db_path)
1408            .await
1409            .unwrap();
1410
1411        let config = DatabasePoolConfig {
1412            max_connections: 5,
1413            min_connections: 1,
1414            connect_timeout: Duration::from_secs(10),
1415            idle_timeout: Duration::from_secs(300),
1416            max_lifetime: Duration::from_secs(900),
1417            test_before_acquire: true,
1418            sqlite_optimizations: SqliteOptimizations::default(),
1419        };
1420
1421        let database = ThingsDatabase::new_with_config(db_path, config)
1422            .await
1423            .unwrap();
1424        let pool = database.pool();
1425        assert!(!pool.is_closed());
1426    }
1427
1428    #[tokio::test]
1429    async fn test_database_error_handling_invalid_path() {
1430        // Test with non-existent database path
1431        let result = ThingsDatabase::new(Path::new("/non/existent/path.db")).await;
1432        assert!(result.is_err(), "Should fail with non-existent path");
1433    }
1434
1435    #[tokio::test]
1436    async fn test_database_get_stats() {
1437        let temp_file = NamedTempFile::new().unwrap();
1438        let db_path = temp_file.path();
1439
1440        crate::test_utils::create_test_database(db_path)
1441            .await
1442            .unwrap();
1443        let database = ThingsDatabase::new(db_path).await.unwrap();
1444
1445        let stats = database.get_stats().await.unwrap();
1446        assert!(stats.task_count > 0, "Should have test tasks");
1447        assert!(stats.area_count > 0, "Should have test areas");
1448        assert!(stats.total_items() > 0, "Should have total items");
1449    }
1450
1451    #[tokio::test]
1452    async fn test_database_comprehensive_health_check() {
1453        let temp_file = NamedTempFile::new().unwrap();
1454        let db_path = temp_file.path();
1455
1456        crate::test_utils::create_test_database(db_path)
1457            .await
1458            .unwrap();
1459        let database = ThingsDatabase::new(db_path).await.unwrap();
1460
1461        let health = database.comprehensive_health_check().await.unwrap();
1462        assert!(health.overall_healthy, "Database should be healthy");
1463        assert!(health.pool_health.is_healthy, "Pool should be healthy");
1464        assert!(
1465            health.pool_metrics.is_healthy,
1466            "Pool metrics should be healthy"
1467        );
1468    }
1469}