1use crate::db::models::Event;
2use crate::error::{IntentError, Result};
3use chrono::Utc;
4use sqlx::SqlitePool;
5
6pub struct EventManager<'a> {
7 pool: &'a SqlitePool,
8}
9
10impl<'a> EventManager<'a> {
11 pub fn new(pool: &'a SqlitePool) -> Self {
12 Self { pool }
13 }
14
15 pub async fn add_event(
17 &self,
18 task_id: i64,
19 log_type: &str,
20 discussion_data: &str,
21 ) -> Result<Event> {
22 let task_exists: bool =
24 sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM tasks WHERE id = ?)")
25 .bind(task_id)
26 .fetch_one(self.pool)
27 .await?;
28
29 if !task_exists {
30 return Err(IntentError::TaskNotFound(task_id));
31 }
32
33 let now = Utc::now();
34
35 let result = sqlx::query(
36 r#"
37 INSERT INTO events (task_id, log_type, discussion_data, timestamp)
38 VALUES (?, ?, ?, ?)
39 "#,
40 )
41 .bind(task_id)
42 .bind(log_type)
43 .bind(discussion_data)
44 .bind(now)
45 .execute(self.pool)
46 .await?;
47
48 let id = result.last_insert_rowid();
49
50 Ok(Event {
51 id,
52 task_id,
53 timestamp: now,
54 log_type: log_type.to_string(),
55 discussion_data: discussion_data.to_string(),
56 })
57 }
58
59 pub async fn list_events(
61 &self,
62 task_id: i64,
63 limit: Option<i64>,
64 log_type: Option<String>,
65 since: Option<String>,
66 ) -> Result<Vec<Event>> {
67 let task_exists: bool =
69 sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM tasks WHERE id = ?)")
70 .bind(task_id)
71 .fetch_one(self.pool)
72 .await?;
73
74 if !task_exists {
75 return Err(IntentError::TaskNotFound(task_id));
76 }
77
78 let limit = limit.unwrap_or(100);
79
80 let since_timestamp = if let Some(duration_str) = since {
82 Some(Self::parse_duration(&duration_str)?)
83 } else {
84 None
85 };
86
87 let mut query = String::from(
89 "SELECT id, task_id, timestamp, log_type, discussion_data FROM events WHERE task_id = ?",
90 );
91 let mut conditions = Vec::new();
92
93 if log_type.is_some() {
94 conditions.push("log_type = ?");
95 }
96
97 if since_timestamp.is_some() {
98 conditions.push("timestamp >= ?");
99 }
100
101 if !conditions.is_empty() {
102 query.push_str(" AND ");
103 query.push_str(&conditions.join(" AND "));
104 }
105
106 query.push_str(" ORDER BY timestamp DESC LIMIT ?");
107
108 let mut sql_query = sqlx::query_as::<_, Event>(&query).bind(task_id);
110
111 if let Some(ref typ) = log_type {
112 sql_query = sql_query.bind(typ);
113 }
114
115 if let Some(ts) = since_timestamp {
116 sql_query = sql_query.bind(ts);
117 }
118
119 sql_query = sql_query.bind(limit);
120
121 let events = sql_query.fetch_all(self.pool).await?;
122
123 Ok(events)
124 }
125
126 fn parse_duration(duration: &str) -> Result<chrono::DateTime<Utc>> {
128 let len = duration.len();
129 if len < 2 {
130 return Err(IntentError::InvalidInput(
131 "Duration must be in format like '7d', '24h', or '30m'".to_string(),
132 ));
133 }
134
135 let (num_str, unit) = duration.split_at(len - 1);
136 let num: i64 = num_str.parse().map_err(|_| {
137 IntentError::InvalidInput(format!("Invalid number in duration: {}", num_str))
138 })?;
139
140 let now = Utc::now();
141 let result = match unit {
142 "d" => now - chrono::Duration::days(num),
143 "h" => now - chrono::Duration::hours(num),
144 "m" => now - chrono::Duration::minutes(num),
145 "s" => now - chrono::Duration::seconds(num),
146 _ => {
147 return Err(IntentError::InvalidInput(format!(
148 "Invalid duration unit '{}'. Use 'd' (days), 'h' (hours), 'm' (minutes), or 's' (seconds)",
149 unit
150 )))
151 }
152 };
153
154 Ok(result)
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use crate::tasks::TaskManager;
162 use crate::test_utils::test_helpers::TestContext;
163
164 #[tokio::test]
165 async fn test_add_event() {
166 let ctx = TestContext::new().await;
167 let task_mgr = TaskManager::new(ctx.pool());
168 let event_mgr = EventManager::new(ctx.pool());
169
170 let task = task_mgr.add_task("Test task", None, None).await.unwrap();
171 let event = event_mgr
172 .add_event(task.id, "decision", "Test decision")
173 .await
174 .unwrap();
175
176 assert_eq!(event.task_id, task.id);
177 assert_eq!(event.log_type, "decision");
178 assert_eq!(event.discussion_data, "Test decision");
179 }
180
181 #[tokio::test]
182 async fn test_add_event_nonexistent_task() {
183 let ctx = TestContext::new().await;
184 let event_mgr = EventManager::new(ctx.pool());
185
186 let result = event_mgr.add_event(999, "decision", "Test").await;
187 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
188 }
189
190 #[tokio::test]
191 async fn test_list_events() {
192 let ctx = TestContext::new().await;
193 let task_mgr = TaskManager::new(ctx.pool());
194 let event_mgr = EventManager::new(ctx.pool());
195
196 let task = task_mgr.add_task("Test task", None, None).await.unwrap();
197
198 event_mgr
200 .add_event(task.id, "decision", "Decision 1")
201 .await
202 .unwrap();
203 event_mgr
204 .add_event(task.id, "blocker", "Blocker 1")
205 .await
206 .unwrap();
207 event_mgr
208 .add_event(task.id, "milestone", "Milestone 1")
209 .await
210 .unwrap();
211
212 let events = event_mgr
213 .list_events(task.id, None, None, None)
214 .await
215 .unwrap();
216 assert_eq!(events.len(), 3);
217
218 assert_eq!(events[0].log_type, "milestone");
220 assert_eq!(events[1].log_type, "blocker");
221 assert_eq!(events[2].log_type, "decision");
222 }
223
224 #[tokio::test]
225 async fn test_list_events_with_limit() {
226 let ctx = TestContext::new().await;
227 let task_mgr = TaskManager::new(ctx.pool());
228 let event_mgr = EventManager::new(ctx.pool());
229
230 let task = task_mgr.add_task("Test task", None, None).await.unwrap();
231
232 for i in 0..5 {
234 event_mgr
235 .add_event(task.id, "test", &format!("Event {}", i))
236 .await
237 .unwrap();
238 }
239
240 let events = event_mgr
241 .list_events(task.id, Some(3), None, None)
242 .await
243 .unwrap();
244 assert_eq!(events.len(), 3);
245 }
246
247 #[tokio::test]
248 async fn test_list_events_nonexistent_task() {
249 let ctx = TestContext::new().await;
250 let event_mgr = EventManager::new(ctx.pool());
251
252 let result = event_mgr.list_events(999, None, None, None).await;
253 assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
254 }
255
256 #[tokio::test]
257 async fn test_list_events_empty() {
258 let ctx = TestContext::new().await;
259 let task_mgr = TaskManager::new(ctx.pool());
260 let event_mgr = EventManager::new(ctx.pool());
261
262 let task = task_mgr.add_task("Test task", None, None).await.unwrap();
263
264 let events = event_mgr
265 .list_events(task.id, None, None, None)
266 .await
267 .unwrap();
268 assert_eq!(events.len(), 0);
269 }
270}