vkteams_bot_cli/commands/
storage.rs

1//! Storage and database commands
2
3use crate::commands::{Command, OutputFormat};
4use crate::errors::prelude::Result as CliResult;
5use crate::output::{CliResponse, OutputFormatter};
6use async_trait::async_trait;
7use clap::Subcommand;
8use serde_json::json;
9use vkteams_bot::prelude::*;
10
11use vkteams_bot::storage::config::{DatabaseConfig, StorageSettings};
12use vkteams_bot::storage::{StorageConfig, StorageManager};
13
14#[derive(Debug, Clone, Subcommand)]
15pub enum StorageCommands {
16    /// Database operations
17    Database {
18        #[command(subcommand)]
19        action: DatabaseAction,
20    },
21    /// Search operations
22    Search {
23        #[command(subcommand)]
24        action: SearchAction,
25    },
26    /// Context management
27    Context {
28        #[command(subcommand)]
29        action: ContextAction,
30    },
31}
32
33#[derive(Debug, Clone, Subcommand)]
34pub enum DatabaseAction {
35    /// Initialize database and run migrations
36    Init,
37    /// Get database statistics
38    Stats {
39        #[arg(long)]
40        chat_id: Option<String>,
41        #[arg(long)]
42        since: Option<String>,
43    },
44    /// Clean up old events
45    Cleanup {
46        #[arg(long, default_value = "365")]
47        older_than_days: u32,
48    },
49    /// Get vector store performance metrics
50    VectorMetrics,
51    /// Perform vector store maintenance (vacuum, analyze, reindex)
52    VectorMaintenance,
53}
54
55#[derive(Debug, Clone, Subcommand)]
56pub enum SearchAction {
57    /// Semantic search using vector similarity
58    Semantic {
59        /// Search query
60        query: String,
61        #[arg(long)]
62        chat_id: Option<String>,
63        #[arg(long, default_value = "10")]
64        limit: usize,
65    },
66    /// Full-text search in messages
67    Text {
68        /// Search query
69        query: String,
70        #[arg(long)]
71        chat_id: Option<String>,
72        #[arg(long, default_value = "10")]
73        limit: i64,
74    },
75    /// Advanced search with filters
76    Advanced {
77        #[arg(long)]
78        user_id: Option<String>,
79        #[arg(long)]
80        event_type: Option<String>,
81        #[arg(long)]
82        since: Option<String>,
83        #[arg(long)]
84        until: Option<String>,
85        #[arg(long, default_value = "10")]
86        limit: i64,
87    },
88}
89
90#[derive(Debug, Clone, Subcommand)]
91pub enum ContextAction {
92    /// Get conversation context
93    Get {
94        #[arg(long)]
95        chat_id: Option<String>,
96        #[arg(long, value_enum, default_value = "recent")]
97        context_type: ContextType,
98        #[arg(long)]
99        timeframe: Option<String>,
100    },
101    /// Create new context
102    Create {
103        #[arg(long)]
104        chat_id: String,
105        #[arg(long)]
106        summary: String,
107        #[arg(long)]
108        context_type: String,
109    },
110}
111
112#[derive(Debug, Clone, clap::ValueEnum)]
113pub enum ContextType {
114    Recent,
115    Topic,
116    UserProfile,
117}
118
119impl StorageCommands {
120    pub async fn execute_with_output(
121        &self,
122        _bot: &Bot,
123        output_format: &OutputFormat,
124    ) -> CliResult<()> {
125        {
126            let response = match self {
127                StorageCommands::Database { action } => self.handle_database(action).await,
128                StorageCommands::Search { action } => self.handle_search(action).await,
129                StorageCommands::Context { action } => self.handle_context(action).await,
130            };
131
132            OutputFormatter::print(&response, output_format)?;
133
134            if !response.success {
135                std::process::exit(1);
136            }
137        }
138
139        Ok(())
140    }
141
142    pub async fn get_storage_manager(&self) -> std::result::Result<StorageManager, String> {
143        // Try to load storage configuration
144        let config = match self.load_storage_config().await {
145            Ok(config) => config,
146            Err(e) => return Err(format!("Failed to load storage configuration: {e}")),
147        };
148
149        match StorageManager::new(&config).await {
150            Ok(storage) => Ok(storage),
151            Err(e) => Err(format!("Failed to initialize storage manager: {e}")),
152        }
153    }
154
155    pub async fn load_storage_config(&self) -> std::result::Result<StorageConfig, String> {
156        // Try to load from main library configuration first
157        #[cfg(feature = "storage")]
158        {
159            use vkteams_bot::config::get_config;
160
161            // Try to load from configuration file
162            if let Ok(main_config) = get_config() {
163                return Ok(main_config.get_storage_config());
164            }
165        }
166
167        // Fallback to environment-based configuration if main config fails
168        let database_url = std::env::var("DATABASE_URL")
169            .or_else(|_| std::env::var("VKTEAMS_BOT_DATABASE_URL"))
170            .unwrap_or_else(|_| "postgresql://localhost/vkteams_bot".to_string());
171
172        let config = StorageConfig {
173            database: DatabaseConfig {
174                url: database_url,
175                max_connections: 20,
176                connection_timeout: 30,
177                auto_migrate: true,
178                ssl: Default::default(),
179            },
180            settings: StorageSettings {
181                event_retention_days: 365,
182                cleanup_interval_hours: 24,
183                batch_size: 100,
184                max_memory_events: 10000,
185            },
186            ..Default::default()
187        };
188
189        Ok(config)
190    }
191
192    pub async fn handle_database(&self, action: &DatabaseAction) -> CliResponse<serde_json::Value> {
193        let command_name = match action {
194            DatabaseAction::Init => "database-init",
195            DatabaseAction::Stats { .. } => "database-stats",
196            DatabaseAction::Cleanup { .. } => "database-cleanup",
197            DatabaseAction::VectorMetrics => "database-vector-metrics",
198            DatabaseAction::VectorMaintenance => "database-vector-maintenance",
199        };
200
201        let storage = match self.get_storage_manager().await {
202            Ok(storage) => storage,
203            Err(e) => return CliResponse::error(command_name, e.to_string()),
204        };
205
206        match action {
207            DatabaseAction::Init => match storage.initialize().await {
208                Ok(_) => {
209                    let data = json!({
210                        "message": "Database initialized successfully",
211                        "migrations_applied": true
212                    });
213                    CliResponse::success("database-init", data)
214                }
215                Err(e) => CliResponse::error(
216                    "database-init",
217                    format!("Failed to initialize database: {e}"),
218                ),
219            },
220            DatabaseAction::Stats {
221                chat_id,
222                since: _since,
223            } => match storage.get_stats(chat_id.as_deref()).await {
224                Ok(stats) => {
225                    let data = json!({
226                        "total_events": stats.total_events,
227                        "total_messages": stats.total_messages,
228                        "unique_chats": stats.unique_chats,
229                        "unique_users": stats.unique_users,
230                        "events_last_24h": stats.events_last_24h,
231                        "events_last_week": stats.events_last_week,
232                        "oldest_event": stats.oldest_event,
233                        "newest_event": stats.newest_event,
234                        "storage_size_bytes": stats.storage_size_bytes
235                    });
236                    CliResponse::success("database-stats", data)
237                }
238                Err(e) => CliResponse::error("database-stats", format!("Failed to get stats: {e}")),
239            },
240            DatabaseAction::Cleanup { older_than_days } => {
241                match storage.cleanup_old_data(*older_than_days).await {
242                    Ok(deleted_count) => {
243                        let data = json!({
244                            "deleted_events": deleted_count,
245                            "older_than_days": older_than_days
246                        });
247                        CliResponse::success("database-cleanup", data)
248                    }
249                    Err(e) => {
250                        CliResponse::error("database-cleanup", format!("Failed to cleanup: {e}"))
251                    }
252                }
253            }
254            DatabaseAction::VectorMetrics => {
255                #[cfg(feature = "vector-search")]
256                {
257                    match storage.get_vector_metrics().await {
258                        Ok(Some(metrics)) => {
259                            let data = json!({
260                                "collection_name": metrics.collection_name,
261                                "total_documents": metrics.total_documents,
262                                "total_size_bytes": metrics.total_size_bytes,
263                                "total_size_mb": metrics.total_size_bytes as f64 / 1024.0 / 1024.0,
264                                "index_size_bytes": metrics.index_size_bytes,
265                                "index_size_mb": metrics.index_size_bytes as f64 / 1024.0 / 1024.0,
266                                "dimensions": metrics.dimensions,
267                                "performance": {
268                                    "total_queries": metrics.total_queries,
269                                    "failed_queries": metrics.failed_queries,
270                                    "success_rate": if metrics.total_queries > 0 {
271                                        1.0 - (metrics.failed_queries as f64 / metrics.total_queries as f64)
272                                    } else { 0.0 },
273                                    "avg_query_time_ms": metrics.avg_query_time_ms,
274                                    "last_query_time_ms": metrics.last_query_time_ms
275                                },
276                                "index_usage": {
277                                    "index_scans": metrics.index_usage.index_scans,
278                                    "index_tuples_read": metrics.index_usage.index_tuples_read,
279                                    "index_tuples_fetched": metrics.index_usage.index_tuples_fetched,
280                                    "cache_hit_ratio": metrics.index_usage.cache_hit_ratio,
281                                    "index_blocks_read": metrics.index_usage.index_blocks_read,
282                                    "index_blocks_hit": metrics.index_usage.index_blocks_hit
283                                },
284                                "last_maintenance": metrics.last_maintenance
285                            });
286                            CliResponse::success("database-vector-metrics", data)
287                        }
288                        Ok(None) => CliResponse::error(
289                            "database-vector-metrics",
290                            "Vector store not configured",
291                        ),
292                        Err(e) => CliResponse::error(
293                            "database-vector-metrics",
294                            format!("Failed to get vector metrics: {e}"),
295                        ),
296                    }
297                }
298                #[cfg(not(feature = "vector-search"))]
299                {
300                    CliResponse::error(
301                        "database-vector-metrics",
302                        "Vector search feature not enabled",
303                    )
304                }
305            }
306            DatabaseAction::VectorMaintenance => {
307                #[cfg(feature = "vector-search")]
308                {
309                    match storage.perform_vector_maintenance().await {
310                        Ok(()) => {
311                            let data = json!({
312                                "message": "Vector store maintenance completed successfully",
313                                "operations": ["VACUUM ANALYZE", "REINDEX"],
314                                "timestamp": chrono::Utc::now().to_rfc3339()
315                            });
316                            CliResponse::success("database-vector-maintenance", data)
317                        }
318                        Err(e) => CliResponse::error(
319                            "database-vector-maintenance",
320                            format!("Failed to perform maintenance: {e}"),
321                        ),
322                    }
323                }
324                #[cfg(not(feature = "vector-search"))]
325                {
326                    CliResponse::error(
327                        "database-vector-maintenance",
328                        "Vector search feature not enabled",
329                    )
330                }
331            }
332        }
333    }
334
335    pub async fn handle_search(&self, action: &SearchAction) -> CliResponse<serde_json::Value> {
336        let command_name = match action {
337            SearchAction::Semantic { .. } => "search-semantic",
338            SearchAction::Text { .. } => "search-text",
339            SearchAction::Advanced { .. } => "search-advanced",
340        };
341
342        let storage = match self.get_storage_manager().await {
343            Ok(storage) => storage,
344            Err(e) => return CliResponse::error(command_name, e.to_string()),
345        };
346
347        match action {
348            SearchAction::Semantic {
349                query,
350                chat_id,
351                limit,
352            } => {
353                #[cfg(feature = "vector-search")]
354                {
355                    match storage
356                        .search_similar_events(query, chat_id.as_deref(), *limit)
357                        .await
358                    {
359                        Ok(results) => {
360                            let data = json!({
361                                "query": query,
362                                "results_count": results.len(),
363                                "results": results.into_iter().map(|r| json!({
364                                    "id": r.id,
365                                    "content": r.content,
366                                    "metadata": r.metadata,
367                                    "score": r.score,
368                                    "created_at": r.created_at
369                                })).collect::<Vec<_>>()
370                            });
371                            CliResponse::success("search-semantic", data)
372                        }
373                        Err(e) => CliResponse::error(
374                            "search-semantic",
375                            format!("Semantic search failed: {e}"),
376                        ),
377                    }
378                }
379                #[cfg(not(feature = "vector-search"))]
380                {
381                    let _ = (query, chat_id, limit); // Avoid unused variable warnings
382                    CliResponse::error("search-semantic", "Vector search feature not enabled")
383                }
384            }
385            SearchAction::Text {
386                query,
387                chat_id,
388                limit,
389            } => {
390                match storage
391                    .search_messages(query, chat_id.as_deref(), *limit)
392                    .await
393                {
394                    Ok(messages) => {
395                        let data = json!({
396                            "query": query,
397                            "results_count": messages.len(),
398                            "messages": messages.into_iter().map(|m| json!({
399                                "id": m.id,
400                                "message_id": m.message_id,
401                                "user_id": m.user_id,
402                                "text": m.text,
403                                "timestamp": m.timestamp,
404                                "chat_id": m.chat_id
405                            })).collect::<Vec<_>>()
406                        });
407                        CliResponse::success("search-text", data)
408                    }
409                    Err(e) => CliResponse::error("search-text", format!("Search failed: {e}")),
410                }
411            }
412            SearchAction::Advanced {
413                user_id,
414                event_type,
415                since,
416                until,
417                limit,
418            } => {
419                // Parse date filters
420                let since_date = match since.as_ref().map(|s| parse_datetime(s)) {
421                    Some(Ok(date)) => Some(date),
422                    Some(Err(_)) => {
423                        return CliResponse::error(
424                            "search-advanced",
425                            "Invalid 'since' date format. Use ISO 8601 format (e.g., 2023-01-01T00:00:00Z)",
426                        );
427                    }
428                    None => None,
429                };
430
431                let until_date = match until.as_ref().map(|s| parse_datetime(s)) {
432                    Some(Ok(date)) => Some(date),
433                    Some(Err(_)) => {
434                        return CliResponse::error(
435                            "search-advanced",
436                            "Invalid 'until' date format. Use ISO 8601 format (e.g., 2023-01-01T00:00:00Z)",
437                        );
438                    }
439                    None => None,
440                };
441
442                match storage
443                    .search_events_advanced(
444                        user_id.as_deref(),
445                        event_type.as_deref(),
446                        since_date,
447                        until_date,
448                        *limit,
449                    )
450                    .await
451                {
452                    Ok(events) => {
453                        let data = json!({
454                            "filters": {
455                                "user_id": user_id,
456                                "event_type": event_type,
457                                "since": since,
458                                "until": until,
459                                "limit": limit
460                            },
461                            "results_count": events.len(),
462                            "events": events.into_iter().map(|e| json!({
463                                "id": e.id,
464                                "event_id": e.event_id,
465                                "event_type": e.event_type,
466                                "chat_id": e.chat_id,
467                                "user_id": e.user_id,
468                                "timestamp": e.timestamp,
469                                "processed_data": e.processed_data
470                            })).collect::<Vec<_>>()
471                        });
472                        CliResponse::success("search-advanced", data)
473                    }
474                    Err(e) => CliResponse::error(
475                        "search-advanced",
476                        format!("Advanced search failed: {e}"),
477                    ),
478                }
479            }
480        }
481    }
482
483    pub async fn handle_context(&self, action: &ContextAction) -> CliResponse<serde_json::Value> {
484        let command_name = match action {
485            ContextAction::Get { .. } => "context-get",
486            ContextAction::Create { .. } => "context-create",
487        };
488
489        let storage = match self.get_storage_manager().await {
490            Ok(storage) => storage,
491            Err(e) => return CliResponse::error(command_name, e.to_string()),
492        };
493
494        match action {
495            ContextAction::Get {
496                chat_id,
497                context_type: _,
498                timeframe: _,
499            } => {
500                let default_chat_id =
501                    std::env::var("VKTEAMS_BOT_CHAT_ID").unwrap_or_else(|_| "default".to_string());
502                let chat_id_ref = chat_id.as_deref().unwrap_or(&default_chat_id);
503
504                // Get recent events as context
505                match storage.get_recent_events(Some(chat_id_ref), None, 20).await {
506                    Ok(events) => {
507                        let data = json!({
508                            "chat_id": chat_id_ref,
509                            "context_type": "recent",
510                            "events_count": events.len(),
511                            "events": events.into_iter().map(|e| json!({
512                                "id": e.id,
513                                "event_id": e.event_id,
514                                "event_type": e.event_type,
515                                "timestamp": e.timestamp,
516                                "user_id": e.user_id
517                            })).collect::<Vec<_>>()
518                        });
519                        CliResponse::success("context-get", data)
520                    }
521                    Err(e) => {
522                        CliResponse::error("context-get", format!("Failed to get context: {e}"))
523                    }
524                }
525            }
526            ContextAction::Create {
527                chat_id,
528                summary,
529                context_type,
530            } => {
531                // Create a context document based on the provided summary
532                let context_id = uuid::Uuid::new_v4().to_string();
533
534                // Store context as a vector document if vector search is enabled
535                #[cfg(feature = "vector-search")]
536                {
537                    use std::collections::HashMap;
538                    use vkteams_bot::storage::VectorDocument;
539
540                    let mut metadata_map = HashMap::new();
541                    metadata_map.insert(
542                        "chat_id".to_string(),
543                        serde_json::Value::String(chat_id.clone()),
544                    );
545                    metadata_map.insert(
546                        "context_type".to_string(),
547                        serde_json::Value::String(format!("{context_type:?}")),
548                    );
549                    metadata_map.insert(
550                        "created_at".to_string(),
551                        serde_json::Value::String(chrono::Utc::now().to_rfc3339()),
552                    );
553
554                    let metadata =
555                        serde_json::to_value(metadata_map).unwrap_or(serde_json::Value::Null);
556
557                    // Get dimensions from storage configuration
558                    let dimensions = storage.get_embedding_dimensions();
559
560                    let document = VectorDocument {
561                        id: context_id.clone(),
562                        content: summary.clone(),
563                        metadata,
564                        embedding: pgvector::Vector::from(vec![0.0; dimensions]), // Placeholder embedding with correct dimensions
565                        created_at: chrono::Utc::now(),
566                    };
567
568                    match storage.store_vector_document(&document).await {
569                        Ok(_) => {
570                            let data = json!({
571                                "context_id": context_id,
572                                "chat_id": chat_id,
573                                "summary": summary,
574                                "context_type": format!("{:?}", context_type),
575                                "created_at": chrono::Utc::now().to_rfc3339(),
576                                "status": "created"
577                            });
578                            CliResponse::success("context-create", data)
579                        }
580                        Err(e) => CliResponse::error(
581                            "context-create",
582                            format!("Failed to create context: {e}"),
583                        ),
584                    }
585                }
586
587                #[cfg(not(feature = "vector-search"))]
588                {
589                    let _ = (chat_id, summary, context_type); // Avoid unused variable warnings
590                    CliResponse::error(
591                        "context-create",
592                        "Vector search feature not enabled. Context creation requires vector storage.",
593                    )
594                }
595            }
596        }
597    }
598}
599
600#[async_trait]
601impl Command for StorageCommands {
602    async fn execute(&self, bot: &Bot) -> CliResult<()> {
603        self.execute_with_output(bot, &OutputFormat::Pretty).await
604    }
605
606    fn name(&self) -> &'static str {
607        match self {
608            StorageCommands::Database { .. } => "database",
609            StorageCommands::Search { .. } => "search",
610            StorageCommands::Context { .. } => "context",
611        }
612    }
613
614    fn validate(&self) -> CliResult<()> {
615        // Add validation logic here if needed
616        Ok(())
617    }
618}
619
620// Helper function to parse datetime strings
621fn parse_datetime(
622    date_str: &str,
623) -> std::result::Result<chrono::DateTime<chrono::Utc>, &'static str> {
624    use chrono::{DateTime, TimeZone};
625
626    // Try different formats
627    if let Ok(dt) = DateTime::parse_from_rfc3339(date_str) {
628        return Ok(dt.with_timezone(&chrono::Utc));
629    }
630
631    // Try ISO format without timezone
632    if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(date_str, "%Y-%m-%dT%H:%M:%S") {
633        return Ok(chrono::Utc.from_utc_datetime(&dt));
634    }
635
636    // Try date only
637    if let Ok(date) = chrono::NaiveDate::parse_from_str(date_str, "%Y-%m-%d")
638        && let Some(datetime) = date.and_hms_opt(0, 0, 0)
639    {
640        return Ok(chrono::Utc.from_utc_datetime(&datetime));
641    }
642
643    Err("Invalid date format")
644}
645
646#[cfg(test)]
647mod storage_tests;
648
649#[cfg(test)]
650mod tests {
651    use super::*;
652
653    #[test]
654    fn test_storage_commands_name() {
655        let cmd = StorageCommands::Database {
656            action: DatabaseAction::Init,
657        };
658        assert_eq!(cmd.name(), "database");
659
660        let cmd = StorageCommands::Search {
661            action: SearchAction::Text {
662                query: "test".to_string(),
663                chat_id: None,
664                limit: 10,
665            },
666        };
667        assert_eq!(cmd.name(), "search");
668    }
669
670    #[test]
671    fn test_context_type_enum() {
672        let context_type = ContextType::Recent;
673        assert!(matches!(context_type, ContextType::Recent));
674    }
675
676    #[test]
677    fn test_parse_datetime() {
678        // Test RFC3339 format
679        assert!(parse_datetime("2023-01-01T00:00:00Z").is_ok());
680
681        // Test ISO format without timezone
682        assert!(parse_datetime("2023-01-01T00:00:00").is_ok());
683
684        // Test date only
685        assert!(parse_datetime("2023-01-01").is_ok());
686
687        // Test invalid format
688        assert!(parse_datetime("invalid-date").is_err());
689    }
690
691    #[test]
692    fn test_context_action_variants() {
693        // Test that ContextAction variants are defined correctly
694        let get_action = ContextAction::Get {
695            chat_id: Some("test_chat".to_string()),
696            context_type: ContextType::Recent,
697            timeframe: None,
698        };
699
700        let create_action = ContextAction::Create {
701            chat_id: "test_chat".to_string(),
702            summary: "Test summary".to_string(),
703            context_type: "recent".to_string(),
704        };
705
706        // These should match without errors
707        match get_action {
708            ContextAction::Get { .. } => {}
709            _ => panic!("Expected ContextAction::Get"),
710        }
711
712        match create_action {
713            ContextAction::Create { .. } => {}
714            _ => panic!("Expected ContextAction::Create"),
715        }
716    }
717
718    #[test]
719    fn test_database_action_variants() {
720        // Test DatabaseAction variants
721        let init_action = DatabaseAction::Init;
722        let stats_action = DatabaseAction::Stats {
723            chat_id: Some("test_chat".to_string()),
724            since: Some("2023-01-01".to_string()),
725        };
726        let cleanup_action = DatabaseAction::Cleanup {
727            older_than_days: 30,
728        };
729        let vector_metrics_action = DatabaseAction::VectorMetrics;
730        let vector_maintenance_action = DatabaseAction::VectorMaintenance;
731
732        match init_action {
733            DatabaseAction::Init => {}
734            _ => panic!("Expected DatabaseAction::Init"),
735        }
736
737        match stats_action {
738            DatabaseAction::Stats { .. } => {}
739            _ => panic!("Expected DatabaseAction::Stats"),
740        }
741
742        match cleanup_action {
743            DatabaseAction::Cleanup { older_than_days } => {
744                assert_eq!(older_than_days, 30);
745            }
746            _ => panic!("Expected DatabaseAction::Cleanup"),
747        }
748
749        match vector_metrics_action {
750            DatabaseAction::VectorMetrics => {}
751            _ => panic!("Expected DatabaseAction::VectorMetrics"),
752        }
753
754        match vector_maintenance_action {
755            DatabaseAction::VectorMaintenance => {}
756            _ => panic!("Expected DatabaseAction::VectorMaintenance"),
757        }
758    }
759
760    #[test]
761    fn test_search_action_variants() {
762        // Test SearchAction variants
763        let semantic_action = SearchAction::Semantic {
764            query: "test query".to_string(),
765            chat_id: Some("test_chat".to_string()),
766            limit: 5,
767        };
768
769        let text_action = SearchAction::Text {
770            query: "search text".to_string(),
771            chat_id: None,
772            limit: 20,
773        };
774
775        let advanced_action = SearchAction::Advanced {
776            user_id: Some("user123".to_string()),
777            event_type: Some("NewMessage".to_string()),
778            since: Some("2023-01-01".to_string()),
779            until: Some("2023-12-31".to_string()),
780            limit: 50,
781        };
782
783        match semantic_action {
784            SearchAction::Semantic {
785                query,
786                chat_id,
787                limit,
788            } => {
789                assert_eq!(query, "test query");
790                assert_eq!(chat_id, Some("test_chat".to_string()));
791                assert_eq!(limit, 5);
792            }
793            _ => panic!("Expected SearchAction::Semantic"),
794        }
795
796        match text_action {
797            SearchAction::Text {
798                query,
799                chat_id,
800                limit,
801            } => {
802                assert_eq!(query, "search text");
803                assert_eq!(chat_id, None);
804                assert_eq!(limit, 20);
805            }
806            _ => panic!("Expected SearchAction::Text"),
807        }
808
809        match advanced_action {
810            SearchAction::Advanced {
811                user_id,
812                event_type,
813                since,
814                until,
815                limit,
816            } => {
817                assert_eq!(user_id, Some("user123".to_string()));
818                assert_eq!(event_type, Some("NewMessage".to_string()));
819                assert_eq!(since, Some("2023-01-01".to_string()));
820                assert_eq!(until, Some("2023-12-31".to_string()));
821                assert_eq!(limit, 50);
822            }
823            _ => panic!("Expected SearchAction::Advanced"),
824        }
825    }
826
827    #[test]
828    fn test_context_type_enum_values() {
829        // Test all ContextType variants
830        let recent = ContextType::Recent;
831        let topic = ContextType::Topic;
832        let user_profile = ContextType::UserProfile;
833
834        match recent {
835            ContextType::Recent => {}
836            _ => panic!("Expected ContextType::Recent"),
837        }
838
839        match topic {
840            ContextType::Topic => {}
841            _ => panic!("Expected ContextType::Topic"),
842        }
843
844        match user_profile {
845            ContextType::UserProfile => {}
846            _ => panic!("Expected ContextType::UserProfile"),
847        }
848    }
849
850    #[test]
851    fn test_storage_commands_validation() {
852        // Test that storage commands validate correctly
853        let database_cmd = StorageCommands::Database {
854            action: DatabaseAction::Init,
855        };
856        assert!(database_cmd.validate().is_ok());
857
858        let search_cmd = StorageCommands::Search {
859            action: SearchAction::Text {
860                query: "test".to_string(),
861                chat_id: None,
862                limit: 10,
863            },
864        };
865        assert!(search_cmd.validate().is_ok());
866
867        let context_cmd = StorageCommands::Context {
868            action: ContextAction::Get {
869                chat_id: Some("test".to_string()),
870                context_type: ContextType::Recent,
871                timeframe: None,
872            },
873        };
874        assert!(context_cmd.validate().is_ok());
875    }
876
877    #[test]
878    fn test_storage_commands_debug_and_clone() {
879        let cmd = StorageCommands::Database {
880            action: DatabaseAction::Init,
881        };
882
883        // Test Debug trait
884        let debug_str = format!("{cmd:?}");
885        assert!(debug_str.contains("Database"));
886        assert!(debug_str.contains("Init"));
887
888        // Test Clone trait
889        let cloned_cmd = cmd.clone();
890        assert_eq!(cloned_cmd.name(), cmd.name());
891    }
892
893    #[test]
894    fn test_parse_datetime_edge_cases() {
895        // Test more date format edge cases
896        assert!(parse_datetime("2023-12-31T23:59:59Z").is_ok());
897        assert!(parse_datetime("2023-01-01T00:00:00+00:00").is_ok());
898        assert!(parse_datetime("2023-06-15T12:30:45").is_ok());
899        assert!(parse_datetime("2023-02-28").is_ok());
900        assert!(parse_datetime("2024-02-29").is_ok()); // Leap year
901
902        // Invalid formats
903        assert!(parse_datetime("").is_err());
904        assert!(parse_datetime("not-a-date").is_err());
905        assert!(parse_datetime("2023-13-01").is_err()); // Invalid month
906        assert!(parse_datetime("2023-02-30").is_err()); // Invalid day
907    }
908
909    #[tokio::test]
910    async fn test_storage_config_fallback() {
911        let storage_cmd = StorageCommands::Database {
912            action: DatabaseAction::Init,
913        };
914
915        // Test fallback configuration loading
916        let config_result = storage_cmd.load_storage_config().await;
917        assert!(config_result.is_ok());
918
919        let config = config_result.unwrap();
920        assert_eq!(config.database.max_connections, 20);
921        assert_eq!(config.database.connection_timeout, 30);
922        assert!(config.database.auto_migrate);
923        assert_eq!(config.settings.event_retention_days, 365);
924        assert_eq!(config.settings.cleanup_interval_hours, 24);
925        assert_eq!(config.settings.batch_size, 100);
926        assert_eq!(config.settings.max_memory_events, 10000);
927    }
928
929    #[test]
930    fn test_context_action_get_with_different_types() {
931        let recent_action = ContextAction::Get {
932            chat_id: Some("chat1".to_string()),
933            context_type: ContextType::Recent,
934            timeframe: Some("1d".to_string()),
935        };
936
937        let topic_action = ContextAction::Get {
938            chat_id: Some("chat2".to_string()),
939            context_type: ContextType::Topic,
940            timeframe: Some("1w".to_string()),
941        };
942
943        let user_profile_action = ContextAction::Get {
944            chat_id: Some("chat3".to_string()),
945            context_type: ContextType::UserProfile,
946            timeframe: None,
947        };
948
949        match recent_action {
950            ContextAction::Get {
951                context_type: ContextType::Recent,
952                ..
953            } => {}
954            _ => panic!("Expected Recent context type"),
955        }
956
957        match topic_action {
958            ContextAction::Get {
959                context_type: ContextType::Topic,
960                ..
961            } => {}
962            _ => panic!("Expected Topic context type"),
963        }
964
965        match user_profile_action {
966            ContextAction::Get {
967                context_type: ContextType::UserProfile,
968                ..
969            } => {}
970            _ => panic!("Expected UserProfile context type"),
971        }
972    }
973
974    #[test]
975    fn test_storage_commands_name_variants() {
976        let database_cmd = StorageCommands::Database {
977            action: DatabaseAction::Stats {
978                chat_id: None,
979                since: None,
980            },
981        };
982        assert_eq!(database_cmd.name(), "database");
983
984        let search_cmd = StorageCommands::Search {
985            action: SearchAction::Semantic {
986                query: "test".to_string(),
987                chat_id: None,
988                limit: 10,
989            },
990        };
991        assert_eq!(search_cmd.name(), "search");
992
993        let context_cmd = StorageCommands::Context {
994            action: ContextAction::Create {
995                chat_id: "test".to_string(),
996                summary: "summary".to_string(),
997                context_type: "topic".to_string(),
998            },
999        };
1000        assert_eq!(context_cmd.name(), "context");
1001    }
1002}