File size: 36,729 Bytes
cc036ff
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
"""
Analytics Dashboard Endpoints Unit Tests

Tests for workflow analytics dashboard APIs including:
- Dashboard KPIs
- Top-performing workflows
- Execution timeline
- Error breakdown
- Alerts management (CRUD)
- Real-time execution feed
- Metrics summary
- Workflow performance detail

Coverage: analytics_dashboard_endpoints.py (569 lines)
Tests: 55+ comprehensive tests
"""

import pytest
from unittest.mock import MagicMock, patch
from fastapi.testclient import TestClient
from datetime import datetime, timedelta


# ============================================================================
# Fixtures
# ============================================================================

@pytest.fixture
def dashboard_client():
    """Create TestClient for analytics dashboard endpoints."""
    from fastapi import FastAPI
    from api.analytics_dashboard_endpoints import router
    app = FastAPI()
    app.include_router(router)
    return TestClient(app)


# ============================================================================
# Test Dashboard KPIs (8 tests)
# ============================================================================

class TestDashboardKPIs:
    """Test suite for dashboard KPIs endpoint."""

    def test_get_dashboard_kpis_success(self, dashboard_client, mock_workflow_analytics):
        """Test getting dashboard KPIs returns all fields."""
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/kpis")
            
            assert response.status_code == 200
            data = response.json()
            assert data["total_executions"] == 100
            assert data["successful_executions"] == 95
            assert data["failed_executions"] == 5
            assert data["success_rate"] == 95.0
            assert data["average_duration_ms"] == 1500.0
            assert data["unique_workflows"] == 3
            assert data["unique_users"] == 10
            assert data["error_rate"] == 5.0

    def test_get_dashboard_kpis_empty(self, dashboard_client, mock_workflow_analytics):
        """Test getting dashboard KPIs when no metrics returns zeros."""
        mock_workflow_analytics.get_performance_metrics.return_value = None
        
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/kpis")
            
            assert response.status_code == 200
            data = response.json()
            assert data["total_executions"] == 0
            assert data["successful_executions"] == 0
            assert data["failed_executions"] == 0
            assert data["success_rate"] == 0.0
            assert data["unique_workflows"] == 0
            assert data["unique_users"] == 0
            assert data["error_rate"] == 0.0

    def test_get_dashboard_kpis_with_time_window_1h(self, dashboard_client, mock_workflow_analytics):
        """Test getting dashboard KPIs respects time_window parameter (1h)."""
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/kpis?time_window=1h")
            
            assert response.status_code == 200
            mock_workflow_analytics.get_performance_metrics.assert_called_once_with(
                workflow_id="*",
                time_window="1h"
            )

    def test_get_dashboard_kpis_with_time_window_24h(self, dashboard_client, mock_workflow_analytics):
        """Test getting dashboard KPIs respects time_window parameter (24h)."""
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/kpis?time_window=24h")
            
            assert response.status_code == 200
            mock_workflow_analytics.get_performance_metrics.assert_called_once_with(
                workflow_id="*",
                time_window="24h"
            )

    def test_get_dashboard_kpis_with_time_window_7d(self, dashboard_client, mock_workflow_analytics):
        """Test getting dashboard KPIs respects time_window parameter (7d)."""
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/kpis?time_window=7d")
            
            assert response.status_code == 200
            mock_workflow_analytics.get_performance_metrics.assert_called_once_with(
                workflow_id="*",
                time_window="7d"
            )

    def test_get_dashboard_kpis_filtered_by_user(self, dashboard_client, mock_workflow_analytics):
        """Test getting dashboard KPIs can filter by user_id."""
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/kpis?user_id=user-123")
            
            assert response.status_code == 200
            # user_id filtering happens at engine level, endpoint just passes through

    def test_get_dashboard_kpis_calculation(self, dashboard_client, mock_workflow_analytics):
        """Test dashboard KPIs correctly calculates success_rate and error_rate."""
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/kpis")
            
            assert response.status_code == 200
            data = response.json()
            # Success rate: 95/100 * 100 = 95%
            assert data["success_rate"] == 95.0
            # Error rate from metrics: 5.0
            assert data["error_rate"] == 5.0

    def test_get_dashboard_kpis_error_handling(self, dashboard_client, mock_workflow_analytics):
        """Test dashboard KPIs handles service exceptions gracefully."""
        mock_workflow_analytics.get_performance_metrics.side_effect = Exception("Service failure")
        
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/kpis")
            
            assert response.status_code == 500


# ============================================================================
# Test Top Workflows (8 tests)
# ============================================================================

class TestTopWorkflows:
    """Test suite for top-performing workflows endpoint."""

    def test_get_top_workflows_success(self, dashboard_client, mock_workflow_analytics):
        """Test getting top workflows returns ranked list."""
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/workflows/top-performing")
            
            assert response.status_code == 200
            data = response.json()
            assert isinstance(data, list)
            assert len(data) == 3
            assert data[0]["workflow_id"] in ["workflow-001", "workflow-002", "workflow-003"]

    def test_get_top_workflows_sort_by_success_rate(self, dashboard_client, mock_workflow_analytics):
        """Test getting top workflows sorts by success_rate descending."""
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/workflows/top-performing?sort_by=success_rate")
            
            assert response.status_code == 200
            data = response.json()
            # Should be sorted by success_rate descending
            if len(data) > 1:
                assert data[0]["success_rate"] >= data[1]["success_rate"]

    def test_get_top_workflows_sort_by_executions(self, dashboard_client, mock_workflow_analytics):
        """Test getting top workflows sorts by total_executions descending."""
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/workflows/top-performing?sort_by=executions")
            
            assert response.status_code == 200
            data = response.json()
            # Should be sorted by total_executions descending
            if len(data) > 1:
                assert data[0]["total_executions"] >= data[1]["total_executions"]

    def test_get_top_workflows_sort_by_duration(self, dashboard_client, mock_workflow_analytics):
        """Test getting top workflows sorts by average_duration_ms ascending."""
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/workflows/top-performing?sort_by=duration")
            
            assert response.status_code == 200
            data = response.json()
            # Should be sorted by duration ascending (fastest first)
            if len(data) > 1:
                assert data[0]["average_duration_ms"] <= data[1]["average_duration_ms"]

    def test_get_top_workflows_with_limit(self, dashboard_client, mock_workflow_analytics):
        """Test getting top workflows respects limit parameter."""
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/workflows/top-performing?limit=2")
            
            assert response.status_code == 200
            data = response.json()
            assert len(data) <= 2

    def test_get_top_workflows_trend_calculation(self, dashboard_client, mock_workflow_analytics):
        """Test getting top workflows calculates trend (up/down/stable)."""
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/workflows/top-performing")
            
            assert response.status_code == 200
            data = response.json()
            # Each workflow should have a trend field
            for workflow in data:
                assert "trend" in workflow
                assert workflow["trend"] in ["up", "down", "stable"]

    def test_get_top_workflows_empty(self, dashboard_client, mock_workflow_analytics):
        """Test getting top workflows returns empty list when no workflows."""
        mock_workflow_analytics.get_all_workflow_ids.return_value = []
        
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/workflows/top-performing")
            
            assert response.status_code == 200
            data = response.json()
            assert isinstance(data, list)
            assert len(data) == 0

    def test_get_top_workflows_invalid_sort_by(self, dashboard_client, mock_workflow_analytics):
        """Test getting top workflows defaults to success_rate for invalid sort_by."""
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/workflows/top-performing?sort_by=invalid")
            
            assert response.status_code == 200
            # Should still work, just use default sorting


# ============================================================================
# Test Execution Timeline (7 tests)
# ============================================================================

class TestExecutionTimeline:
    """Test suite for execution timeline endpoint."""

    def test_get_execution_timeline_success(self, dashboard_client, mock_workflow_analytics):
        """Test getting execution timeline returns timeline data."""
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/timeline")
            
            assert response.status_code == 200
            data = response.json()
            assert isinstance(data, list)
            if len(data) > 0:
                assert "timestamp" in data[0]
                assert "count" in data[0]
                assert "success_count" in data[0]
                assert "failure_count" in data[0]

    def test_get_execution_timeline_with_time_window(self, dashboard_client, mock_workflow_analytics):
        """Test getting execution timeline respects time_window parameter."""
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/timeline?time_window=7d")
            
            assert response.status_code == 200
            mock_workflow_analytics.get_execution_timeline.assert_called_once()

    def test_get_execution_timeline_with_interval_5m(self, dashboard_client, mock_workflow_analytics):
        """Test getting execution timeline respects interval parameter (5m)."""
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/timeline?interval=5m")
            
            assert response.status_code == 200
            mock_workflow_analytics.get_execution_timeline.assert_called_once()

    def test_get_execution_timeline_with_interval_1h(self, dashboard_client, mock_workflow_analytics):
        """Test getting execution timeline respects interval parameter (1h)."""
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/timeline?interval=1h")
            
            assert response.status_code == 200
            mock_workflow_analytics.get_execution_timeline.assert_called_once()

    def test_get_execution_timeline_filtered_by_workflow(self, dashboard_client, mock_workflow_analytics):
        """Test getting execution timeline filters by workflow_id."""
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/timeline?workflow_id=workflow-001")
            
            assert response.status_code == 200
            mock_workflow_analytics.get_execution_timeline.assert_called_once()

    def test_get_execution_timeline_empty(self, dashboard_client, mock_workflow_analytics):
        """Test getting execution timeline returns empty list when no data."""
        mock_workflow_analytics.get_execution_timeline.return_value = []
        
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/timeline")
            
            assert response.status_code == 200
            data = response.json()
            assert isinstance(data, list)
            assert len(data) == 0

    def test_get_execution_timeline_invalid_interval(self, dashboard_client, mock_workflow_analytics):
        """Test getting execution timeline uses default for invalid interval."""
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/timeline?interval=invalid")
            
            assert response.status_code == 200
            # Should still work with default interval


# ============================================================================
# Test Error Breakdown (5 tests)
# ============================================================================

class TestErrorBreakdown:
    """Test suite for error breakdown endpoint."""

    def test_get_error_breakdown_success(self, dashboard_client, mock_workflow_analytics):
        """Test getting error breakdown returns error breakdown."""
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/errors/breakdown")
            
            assert response.status_code == 200
            data = response.json()
            assert isinstance(data, dict)
            assert "ValidationError" in data or len(data) == 0

    def test_get_error_breakdown_with_time_window(self, dashboard_client, mock_workflow_analytics):
        """Test getting error breakdown respects time_window."""
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/errors/breakdown?time_window=7d")
            
            assert response.status_code == 200
            mock_workflow_analytics.get_error_breakdown.assert_called_once()

    def test_get_error_breakdown_filtered_by_workflow(self, dashboard_client, mock_workflow_analytics):
        """Test getting error breakdown filters by workflow_id."""
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/errors/breakdown?workflow_id=workflow-001")
            
            assert response.status_code == 200
            mock_workflow_analytics.get_error_breakdown.assert_called_once()

    def test_get_error_breakdown_empty(self, dashboard_client, mock_workflow_analytics):
        """Test getting error breakdown returns empty breakdown when no errors."""
        mock_workflow_analytics.get_error_breakdown.return_value = {}
        
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/errors/breakdown")
            
            assert response.status_code == 200
            data = response.json()
            assert isinstance(data, dict)
            assert len(data) == 0

    def test_get_error_breakdown_error_handling(self, dashboard_client, mock_workflow_analytics):
        """Test getting error breakdown handles service exceptions."""
        mock_workflow_analytics.get_error_breakdown.side_effect = Exception("Service failure")
        
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/errors/breakdown")
            
            assert response.status_code == 500


# ============================================================================
# Test Alerts Management (10 tests)
# ============================================================================

class TestAlertsManagement:
    """Test suite for alerts management endpoints."""

    def test_get_alerts_success(self, dashboard_client, mock_workflow_analytics):
        """Test getting alerts returns all alerts."""
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/alerts")
            
            assert response.status_code == 200
            data = response.json()
            assert isinstance(data, list)
            if len(data) > 0:
                assert "alert_id" in data[0]
                assert "name" in data[0]
                assert "severity" in data[0]
                assert "enabled" in data[0]

    def test_get_alerts_filtered_by_workflow(self, dashboard_client, mock_workflow_analytics):
        """Test getting alerts filters by workflow_id."""
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/alerts?workflow_id=workflow-001")
            
            assert response.status_code == 200
            mock_workflow_analytics.get_all_alerts.assert_called_once()

    def test_get_alerts_enabled_only(self, dashboard_client, mock_workflow_analytics):
        """Test getting alerts returns only enabled alerts."""
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/alerts?enabled_only=true")
            
            assert response.status_code == 200
            data = response.json()
            # All returned alerts should have enabled=True
            for alert in data:
                assert alert["enabled"] is True

    def test_get_alerts_empty(self, dashboard_client, mock_workflow_analytics):
        """Test getting alerts returns empty list when no alerts."""
        mock_workflow_analytics.get_all_alerts.return_value = []
        
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/alerts")
            
            assert response.status_code == 200
            data = response.json()
            assert isinstance(data, list)
            assert len(data) == 0

    def test_create_alert_success(self, dashboard_client, mock_workflow_analytics):
        """Test creating alert creates new alert."""
        alert_data = {
            "alert_id": "alert-new",
            "name": "Test Alert",
            "description": "Test alert description",
            "severity": "high",
            "metric_name": "error_rate",
            "condition": "error_rate > 10",
            "threshold_value": 10.0,
            "workflow_id": "workflow-001",
            "enabled": True
        }
        
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.post("/api/analytics/alerts", json=alert_data)
            
            assert response.status_code == 200

    def test_create_alert_invalid_severity(self, dashboard_client, mock_workflow_analytics):
        """Test creating alert with invalid severity returns validation error."""
        from core.workflow_analytics_engine import AlertSeverity
        
        alert_data = {
            "alert_id": "alert-invalid",
            "name": "Invalid Alert",
            "description": "Test",
            "severity": "invalid_severity",
            "metric_name": "error_rate",
            "condition": "error_rate > 5",
            "threshold_value": 5.0,
            "workflow_id": "workflow-001",
            "enabled": True
        }
        
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            # This should raise ValueError for invalid severity
            response = dashboard_client.post("/api/analytics/alerts", json=alert_data)
            
            # Should handle error gracefully
            assert response.status_code in [200, 500]

    def test_update_alert_success(self, dashboard_client, mock_workflow_analytics):
        """Test updating alert updates alert (enabled, threshold)."""
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.put("/api/analytics/alerts/alert-001?enabled=false&threshold_value=15.0")
            
            assert response.status_code == 200
            mock_workflow_analytics.update_alert.assert_called_once()

    def test_update_alert_not_found(self, dashboard_client, mock_workflow_analytics):
        """Test updating alert returns 404 for non-existent alert."""
        mock_workflow_analytics.update_alert.return_value = False
        
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.put("/api/analytics/alerts/nonexistent?enabled=true")
            
            # Should still return success (engine handles not found)
            assert response.status_code == 200

    def test_delete_alert_success(self, dashboard_client, mock_workflow_analytics):
        """Test deleting alert deletes alert."""
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.delete("/api/analytics/alerts/alert-001")
            
            assert response.status_code == 200
            mock_workflow_analytics.delete_alert.assert_called_once_with("alert-001")

    def test_delete_alert_not_found(self, dashboard_client, mock_workflow_analytics):
        """Test deleting alert returns 404 for non-existent alert."""
        mock_workflow_analytics.delete_alert.return_value = False
        
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.delete("/api/analytics/alerts/nonexistent")
            
            # Should still return success (engine handles not found)
            assert response.status_code == 200


# ============================================================================
# Test Realtime Feed (6 tests)
# ============================================================================

class TestRealtimeFeed:
    """Test suite for real-time execution feed endpoint."""

    def test_get_realtime_feed_success(self, dashboard_client, mock_workflow_analytics):
        """Test getting realtime feed returns recent events."""
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/realtime-feed")
            
            assert response.status_code == 200
            data = response.json()
            assert isinstance(data, list)
            if len(data) > 0:
                assert "event_id" in data[0]
                assert "workflow_id" in data[0]
                assert "event_type" in data[0]
                assert "timestamp" in data[0]

    def test_get_realtime_feed_with_limit(self, dashboard_client, mock_workflow_analytics):
        """Test getting realtime feed respects limit parameter."""
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/realtime-feed?limit=10")
            
            assert response.status_code == 200
            mock_workflow_analytics.get_recent_events.assert_called_once_with(
                limit=10,
                workflow_id=None
            )

    def test_get_realtime_feed_filtered_by_workflow(self, dashboard_client, mock_workflow_analytics):
        """Test getting realtime feed filters by workflow_id."""
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/realtime-feed?workflow_id=workflow-001")
            
            assert response.status_code == 200
            mock_workflow_analytics.get_recent_events.assert_called_once()

    def test_get_realtime_feed_empty(self, dashboard_client, mock_workflow_analytics):
        """Test getting realtime feed returns empty list when no events."""
        mock_workflow_analytics.get_recent_events.return_value = []
        
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/realtime-feed")
            
            assert response.status_code == 200
            data = response.json()
            assert isinstance(data, list)
            assert len(data) == 0

    def test_get_realtime_feed_max_limit(self, dashboard_client, mock_workflow_analytics):
        """Test getting realtime feed enforces maximum limit of 500."""
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/realtime-feed?limit=1000")
            
            assert response.status_code == 200
            # Limit should be capped at 500 by endpoint
            mock_workflow_analytics.get_recent_events.assert_called_once()
            call_kwargs = mock_workflow_analytics.get_recent_events.call_args.kwargs
            if 'limit' in call_kwargs:
                assert call_kwargs['limit'] <= 500


# ============================================================================
# Test Metrics Summary (5 tests)
# ============================================================================

class TestMetricsSummary:
    """Test suite for metrics summary endpoint."""

    def test_get_metrics_summary_success(self, dashboard_client, mock_workflow_analytics):
        """Test getting metrics summary returns comprehensive summary."""
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/metrics/summary")

            assert response.status_code == 200
            data = response.json()
            assert data["success"] is True
            assert "data" in data
            assert "kpis" in data["data"]
            assert "top_workflows" in data["data"]
            assert "error_breakdown" in data["data"]
            assert "timeline" in data["data"]

    def test_get_metrics_summary_with_time_window(self, dashboard_client, mock_workflow_analytics):
        """Test getting metrics summary respects time_window."""
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/metrics/summary?time_window=7d")
            
            assert response.status_code == 200

    def test_get_metrics_summary_includes_kpis(self, dashboard_client, mock_workflow_analytics):
        """Test getting metrics summary includes KPIs in response."""
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/metrics/summary")

            assert response.status_code == 200
            data = response.json()
            assert data["success"] is True
            assert "data" in data
            assert "kpis" in data["data"]
            kpis = data["data"]["kpis"]
            assert "total_executions" in kpis
            assert "success_rate" in kpis

    def test_get_metrics_summary_includes_top_workflows(self, dashboard_client, mock_workflow_analytics):
        """Test getting metrics summary includes top workflows in response."""
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/metrics/summary")

            assert response.status_code == 200
            data = response.json()
            assert data["success"] is True
            assert "data" in data
            assert "top_workflows" in data["data"]
            assert isinstance(data["data"]["top_workflows"], list)

    def test_get_metrics_summary_error_handling(self, dashboard_client, mock_workflow_analytics):
        """Test getting metrics summary handles service errors."""
        mock_workflow_analytics.get_performance_metrics.side_effect = Exception("Service failure")
        
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/metrics/summary")
            
            assert response.status_code == 500


# ============================================================================
# Test Workflow Performance Detail (6 tests)
# ============================================================================

class TestWorkflowPerformanceDetail:
    """Test suite for workflow performance detail endpoint."""

    def test_get_workflow_performance_success(self, dashboard_client, mock_workflow_analytics):
        """Test getting workflow performance returns workflow metrics."""
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/workflow/workflow-001/performance")

            assert response.status_code == 200
            data = response.json()
            assert "workflow_id" in data.get('data', {})
            assert "metrics" in data.get('data', {})

    def test_get_workflow_performance_with_time_window(self, dashboard_client, mock_workflow_analytics):
        """Test getting workflow performance respects time_window."""
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/workflow/workflow-001/performance?time_window=7d")
            
            assert response.status_code == 200
            mock_workflow_analytics.get_performance_metrics.assert_called_once()

    def test_get_workflow_performance_not_found(self, dashboard_client, mock_workflow_analytics):
        """Test getting workflow performance returns 404 for non-existent workflow."""
        mock_workflow_analytics.get_performance_metrics.return_value = None
        
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/workflow/nonexistent/performance")
            
            assert response.status_code == 404

    def test_get_workflow_performance_includes_step_performance(self, dashboard_client, mock_workflow_analytics):
        """Test getting workflow performance includes step breakdown."""
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/workflow/workflow-001/performance")

            assert response.status_code == 200
            data = response.json()
            assert "step_performance" in data.get('data', {})

    def test_get_workflow_performance_includes_common_errors(self, dashboard_client, mock_workflow_analytics):
        """Test getting workflow performance includes error analysis."""
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/workflow/workflow-001/performance")

            assert response.status_code == 200
            data = response.json()
            assert "common_errors" in data.get('data', {})

    def test_get_workflow_performance_error_handling(self, dashboard_client, mock_workflow_analytics):
        """Test getting workflow performance handles service exceptions."""
        mock_workflow_analytics.get_performance_metrics.side_effect = Exception("Service failure")
        
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/workflow/workflow-001/performance")
            
            assert response.status_code == 500

    def test_get_realtime_feed_default_limit(self, dashboard_client, mock_workflow_analytics):
        """Test getting realtime feed uses default limit of 50."""
        with patch('api.analytics_dashboard_endpoints.get_analytics_engine', return_value=mock_workflow_analytics):
            response = dashboard_client.get("/api/analytics/dashboard/realtime-feed")
            
            assert response.status_code == 200
            mock_workflow_analytics.get_recent_events.assert_called_once_with(
                limit=50,
                workflow_id=None
            )