1use axum::{extract::Query, http::StatusCode, Json};
7use mockforge_analytics::{
8 AnalyticsDatabase, DriftPercentageMetrics, EndpointCoverage, PersonaCIHit,
9 RealityLevelStaleness, ScenarioUsageMetrics,
10};
11use serde::Deserialize;
12use std::sync::Arc;
13use tracing::{debug, error};
14
15use crate::models::ApiResponse;
16
17#[derive(Clone)]
19pub struct CoverageMetricsState {
20 pub db: Arc<tokio::sync::OnceCell<AnalyticsDatabase>>,
21}
22
23impl CoverageMetricsState {
24 pub fn new(db: AnalyticsDatabase) -> Self {
25 let cell = tokio::sync::OnceCell::new();
26 let _ = cell.set(db);
27 Self { db: Arc::new(cell) }
28 }
29
30 async fn get_db(&self) -> Result<&AnalyticsDatabase, StatusCode> {
31 self.db.get().ok_or_else(|| {
32 error!("Analytics database not initialized");
33 StatusCode::SERVICE_UNAVAILABLE
34 })
35 }
36}
37
38#[derive(Debug, Deserialize)]
40pub struct CoverageQuery {
41 pub workspace_id: Option<String>,
43 pub org_id: Option<String>,
45 pub limit: Option<i64>,
47 pub min_coverage: Option<f64>,
49 pub max_staleness_days: Option<i32>,
51}
52
53pub async fn get_scenario_usage(
57 axum::extract::Extension(state): axum::extract::Extension<CoverageMetricsState>,
58 Query(params): Query<CoverageQuery>,
59) -> Result<Json<ApiResponse<Vec<ScenarioUsageMetrics>>>, StatusCode> {
60 debug!("Getting scenario usage metrics");
61
62 let db = state.get_db().await?;
63 match db
64 .get_scenario_usage(params.workspace_id.as_deref(), params.org_id.as_deref(), params.limit)
65 .await
66 {
67 Ok(metrics) => Ok(Json(ApiResponse::success(metrics))),
68 Err(e) => {
69 error!("Failed to get scenario usage metrics: {}", e);
70 Err(StatusCode::INTERNAL_SERVER_ERROR)
71 }
72 }
73}
74
75pub async fn get_persona_ci_hits(
79 axum::extract::Extension(state): axum::extract::Extension<CoverageMetricsState>,
80 Query(params): Query<CoverageQuery>,
81) -> Result<Json<ApiResponse<Vec<PersonaCIHit>>>, StatusCode> {
82 debug!("Getting persona CI hits");
83
84 let db = state.get_db().await?;
85 match db
86 .get_persona_ci_hits(params.workspace_id.as_deref(), params.org_id.as_deref(), params.limit)
87 .await
88 {
89 Ok(hits) => Ok(Json(ApiResponse::success(hits))),
90 Err(e) => {
91 error!("Failed to get persona CI hits: {}", e);
92 Err(StatusCode::INTERNAL_SERVER_ERROR)
93 }
94 }
95}
96
97pub async fn get_endpoint_coverage(
101 axum::extract::Extension(state): axum::extract::Extension<CoverageMetricsState>,
102 Query(params): Query<CoverageQuery>,
103) -> Result<Json<ApiResponse<Vec<EndpointCoverage>>>, StatusCode> {
104 debug!("Getting endpoint coverage");
105
106 let db = state.get_db().await?;
107 match db
108 .get_endpoint_coverage(
109 params.workspace_id.as_deref(),
110 params.org_id.as_deref(),
111 params.min_coverage,
112 )
113 .await
114 {
115 Ok(coverage) => Ok(Json(ApiResponse::success(coverage))),
116 Err(e) => {
117 error!("Failed to get endpoint coverage: {}", e);
118 Err(StatusCode::INTERNAL_SERVER_ERROR)
119 }
120 }
121}
122
123pub async fn get_reality_level_staleness(
127 axum::extract::Extension(state): axum::extract::Extension<CoverageMetricsState>,
128 Query(params): Query<CoverageQuery>,
129) -> Result<Json<ApiResponse<Vec<RealityLevelStaleness>>>, StatusCode> {
130 debug!("Getting reality level staleness");
131
132 let db = state.get_db().await?;
133 match db
134 .get_reality_level_staleness(
135 params.workspace_id.as_deref(),
136 params.org_id.as_deref(),
137 params.max_staleness_days,
138 )
139 .await
140 {
141 Ok(staleness) => Ok(Json(ApiResponse::success(staleness))),
142 Err(e) => {
143 error!("Failed to get reality level staleness: {}", e);
144 Err(StatusCode::INTERNAL_SERVER_ERROR)
145 }
146 }
147}
148
149pub async fn get_drift_percentage(
153 axum::extract::Extension(state): axum::extract::Extension<CoverageMetricsState>,
154 Query(params): Query<CoverageQuery>,
155) -> Result<Json<ApiResponse<Vec<DriftPercentageMetrics>>>, StatusCode> {
156 debug!("Getting drift percentage metrics");
157
158 let db = state.get_db().await?;
159 match db
160 .get_drift_percentage(
161 params.workspace_id.as_deref(),
162 params.org_id.as_deref(),
163 params.limit,
164 )
165 .await
166 {
167 Ok(metrics) => Ok(Json(ApiResponse::success(metrics))),
168 Err(e) => {
169 error!("Failed to get drift percentage metrics: {}", e);
170 Err(StatusCode::INTERNAL_SERVER_ERROR)
171 }
172 }
173}
174
175pub fn coverage_metrics_router() -> axum::Router {
178 use axum::routing::get;
179
180 axum::Router::new()
181 .route("/scenarios/usage", get(get_scenario_usage))
182 .route("/personas/ci-hits", get(get_persona_ci_hits))
183 .route("/endpoints/coverage", get(get_endpoint_coverage))
184 .route("/reality-levels/staleness", get(get_reality_level_staleness))
185 .route("/drift/percentage", get(get_drift_percentage))
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 #[test]
195 fn test_coverage_query_empty() {
196 let query = CoverageQuery {
197 workspace_id: None,
198 org_id: None,
199 limit: None,
200 min_coverage: None,
201 max_staleness_days: None,
202 };
203
204 assert!(query.workspace_id.is_none());
205 assert!(query.org_id.is_none());
206 assert!(query.limit.is_none());
207 }
208
209 #[test]
210 fn test_coverage_query_with_workspace() {
211 let query = CoverageQuery {
212 workspace_id: Some("ws-123".to_string()),
213 org_id: None,
214 limit: None,
215 min_coverage: None,
216 max_staleness_days: None,
217 };
218
219 assert_eq!(query.workspace_id, Some("ws-123".to_string()));
220 }
221
222 #[test]
223 fn test_coverage_query_with_org() {
224 let query = CoverageQuery {
225 workspace_id: None,
226 org_id: Some("org-456".to_string()),
227 limit: None,
228 min_coverage: None,
229 max_staleness_days: None,
230 };
231
232 assert_eq!(query.org_id, Some("org-456".to_string()));
233 }
234
235 #[test]
236 fn test_coverage_query_with_limit() {
237 let query = CoverageQuery {
238 workspace_id: None,
239 org_id: None,
240 limit: Some(100),
241 min_coverage: None,
242 max_staleness_days: None,
243 };
244
245 assert_eq!(query.limit, Some(100));
246 }
247
248 #[test]
249 fn test_coverage_query_with_min_coverage() {
250 let query = CoverageQuery {
251 workspace_id: None,
252 org_id: None,
253 limit: None,
254 min_coverage: Some(0.75),
255 max_staleness_days: None,
256 };
257
258 assert_eq!(query.min_coverage, Some(0.75));
259 }
260
261 #[test]
262 fn test_coverage_query_with_max_staleness() {
263 let query = CoverageQuery {
264 workspace_id: None,
265 org_id: None,
266 limit: None,
267 min_coverage: None,
268 max_staleness_days: Some(30),
269 };
270
271 assert_eq!(query.max_staleness_days, Some(30));
272 }
273
274 #[test]
275 fn test_coverage_query_full() {
276 let query = CoverageQuery {
277 workspace_id: Some("ws-full".to_string()),
278 org_id: Some("org-full".to_string()),
279 limit: Some(50),
280 min_coverage: Some(0.80),
281 max_staleness_days: Some(14),
282 };
283
284 assert_eq!(query.workspace_id, Some("ws-full".to_string()));
285 assert_eq!(query.org_id, Some("org-full".to_string()));
286 assert_eq!(query.limit, Some(50));
287 assert_eq!(query.min_coverage, Some(0.80));
288 assert_eq!(query.max_staleness_days, Some(14));
289 }
290
291 #[test]
292 fn test_coverage_query_deserialization() {
293 let json = r#"{
294 "workspace_id": "ws-deser",
295 "org_id": "org-deser",
296 "limit": 25,
297 "min_coverage": 0.5,
298 "max_staleness_days": 7
299 }"#;
300
301 let query: CoverageQuery = serde_json::from_str(json).unwrap();
302 assert_eq!(query.workspace_id, Some("ws-deser".to_string()));
303 assert_eq!(query.org_id, Some("org-deser".to_string()));
304 assert_eq!(query.limit, Some(25));
305 assert_eq!(query.min_coverage, Some(0.5));
306 assert_eq!(query.max_staleness_days, Some(7));
307 }
308
309 #[test]
310 fn test_coverage_query_partial_deserialization() {
311 let json = r#"{
312 "workspace_id": "ws-partial"
313 }"#;
314
315 let query: CoverageQuery = serde_json::from_str(json).unwrap();
316 assert_eq!(query.workspace_id, Some("ws-partial".to_string()));
317 assert!(query.org_id.is_none());
318 assert!(query.limit.is_none());
319 }
320
321 #[test]
322 fn test_coverage_query_empty_json() {
323 let json = r#"{}"#;
324
325 let query: CoverageQuery = serde_json::from_str(json).unwrap();
326 assert!(query.workspace_id.is_none());
327 assert!(query.org_id.is_none());
328 assert!(query.limit.is_none());
329 assert!(query.min_coverage.is_none());
330 assert!(query.max_staleness_days.is_none());
331 }
332
333 #[test]
334 fn test_coverage_query_debug() {
335 let query = CoverageQuery {
336 workspace_id: Some("debug-ws".to_string()),
337 org_id: None,
338 limit: Some(10),
339 min_coverage: None,
340 max_staleness_days: None,
341 };
342
343 let debug = format!("{:?}", query);
344 assert!(debug.contains("debug-ws"));
345 assert!(debug.contains("10"));
346 }
347
348 #[test]
351 fn test_coverage_metrics_state_clone() {
352 fn assert_clone<T: Clone>() {}
356 assert_clone::<CoverageMetricsState>();
357 }
358
359 #[test]
362 fn test_coverage_metrics_router_creation() {
363 let router = coverage_metrics_router();
365 let _ = router;
367 }
368
369 #[test]
372 fn test_coverage_query_zero_limit() {
373 let query = CoverageQuery {
374 workspace_id: None,
375 org_id: None,
376 limit: Some(0),
377 min_coverage: None,
378 max_staleness_days: None,
379 };
380
381 assert_eq!(query.limit, Some(0));
382 }
383
384 #[test]
385 fn test_coverage_query_negative_limit() {
386 let query = CoverageQuery {
387 workspace_id: None,
388 org_id: None,
389 limit: Some(-1),
390 min_coverage: None,
391 max_staleness_days: None,
392 };
393
394 assert_eq!(query.limit, Some(-1));
395 }
396
397 #[test]
398 fn test_coverage_query_zero_coverage() {
399 let query = CoverageQuery {
400 workspace_id: None,
401 org_id: None,
402 limit: None,
403 min_coverage: Some(0.0),
404 max_staleness_days: None,
405 };
406
407 assert_eq!(query.min_coverage, Some(0.0));
408 }
409
410 #[test]
411 fn test_coverage_query_full_coverage() {
412 let query = CoverageQuery {
413 workspace_id: None,
414 org_id: None,
415 limit: None,
416 min_coverage: Some(1.0),
417 max_staleness_days: None,
418 };
419
420 assert_eq!(query.min_coverage, Some(1.0));
421 }
422
423 #[test]
424 fn test_coverage_query_over_100_coverage() {
425 let query = CoverageQuery {
427 workspace_id: None,
428 org_id: None,
429 limit: None,
430 min_coverage: Some(1.5),
431 max_staleness_days: None,
432 };
433
434 assert_eq!(query.min_coverage, Some(1.5));
435 }
436
437 #[test]
438 fn test_coverage_query_negative_staleness() {
439 let query = CoverageQuery {
440 workspace_id: None,
441 org_id: None,
442 limit: None,
443 min_coverage: None,
444 max_staleness_days: Some(-7),
445 };
446
447 assert_eq!(query.max_staleness_days, Some(-7));
448 }
449
450 #[test]
451 fn test_coverage_query_large_values() {
452 let query = CoverageQuery {
453 workspace_id: Some("very-long-workspace-id-123456789".to_string()),
454 org_id: Some("very-long-org-id-987654321".to_string()),
455 limit: Some(i64::MAX),
456 min_coverage: Some(f64::MAX),
457 max_staleness_days: Some(i32::MAX),
458 };
459
460 assert!(query.workspace_id.is_some());
461 assert_eq!(query.limit, Some(i64::MAX));
462 }
463
464 #[test]
465 fn test_coverage_query_special_characters() {
466 let query = CoverageQuery {
467 workspace_id: Some("ws-special-!@#$%".to_string()),
468 org_id: Some("org/with/slashes".to_string()),
469 limit: None,
470 min_coverage: None,
471 max_staleness_days: None,
472 };
473
474 assert_eq!(query.workspace_id, Some("ws-special-!@#$%".to_string()));
475 assert_eq!(query.org_id, Some("org/with/slashes".to_string()));
476 }
477
478 #[test]
479 fn test_coverage_query_unicode() {
480 let query = CoverageQuery {
481 workspace_id: Some("workspace-日本語".to_string()),
482 org_id: Some("org-中文".to_string()),
483 limit: None,
484 min_coverage: None,
485 max_staleness_days: None,
486 };
487
488 assert_eq!(query.workspace_id, Some("workspace-日本語".to_string()));
489 assert_eq!(query.org_id, Some("org-中文".to_string()));
490 }
491}