using System.Net; using System.Net.Http.Json; using System.Text.Json; using FluentAssertions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; [Collection("Integration")] public class AlertQualityAnalyticsTests : IAsyncLifetime { private readonly ApiFixture _fixture; private readonly HttpClient _client; private Guid _alertId; public AlertQualityAnalyticsTests(ApiFixture fixture) { _fixture = fixture; _client = fixture.CreateClient(); _client.AsNurse(); } public async Task InitializeAsync() { using var scope = _fixture.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); await DbResetHelper.ResetAsync(db); var patient = new Patient { Id = Guid.NewGuid(), Mrn = "MRN-QA-001", FirstName = "Quality", LastName = "Analytics", DateOfBirth = new DateOnly(1985, 3, 10), Gender = "F", CreatedAt = DateTimeOffset.UtcNow }; var encounter = new Encounter { Id = Guid.NewGuid(), PatientId = patient.Id, EncounterType = EncounterType.Inpatient, Status = EncounterStatus.Active, Department = Department.Icu, AttendingPhysician = "Dr. QA", AdmittedAt = DateTimeOffset.UtcNow, CreatedAt = DateTimeOffset.UtcNow }; var alert = new ClinicalAlert { Id = Guid.NewGuid(), EncounterId = encounter.Id, PatientId = patient.Id, AlertType = AlertType.News2Warning, Severity = AlertSeverity.Warning, Details = "NEWS2 score 6 (MEDIUM).", Status = AlertStatus.Open, TriggeredAt = DateTimeOffset.UtcNow.AddMinutes(-30) }; db.Patients.Add(patient); db.Encounters.Add(encounter); db.ClinicalAlerts.Add(alert); await db.SaveChangesAsync(); _alertId = alert.Id; await _client.PostAsJsonAsync( $"/api/v1/alerts/{_alertId}/acknowledge", new AcknowledgeAlertRequest("Reviewed for QA test.")); } public Task DisposeAsync() => Task.CompletedTask; [Theory] [InlineData("Useful")] [InlineData("TooEarly")] [InlineData("TooLate")] [InlineData("FalsePositive")] [InlineData("MissingContext")] [InlineData("WouldAct")] public async Task SubmitFeedback_AllTypes_Returns201(string feedbackType) { await ResetAcknowledgedAlertAsync(); var resp = await _client.PostAsJsonAsync( $"/api/v1/alerts/{_alertId}/feedback", new { feedbackType, comment = $"QA {feedbackType}" }); resp.StatusCode.Should().Be(HttpStatusCode.Created); var body = await resp.Content.ReadFromJsonAsync(); body!.RootElement.GetProperty("data").GetProperty("id").GetGuid() .Should().NotBe(Guid.Empty); } [Fact] public async Task SubmitFeedback_DuplicateUser_Returns409() { await ResetAcknowledgedAlertAsync(); await _client.PostAsJsonAsync( $"/api/v1/alerts/{_alertId}/feedback", new { feedbackType = "Useful" }); var resp = await _client.PostAsJsonAsync( $"/api/v1/alerts/{_alertId}/feedback", new { feedbackType = "FalsePositive" }); resp.StatusCode.Should().Be(HttpStatusCode.Conflict); var body = await resp.Content.ReadFromJsonAsync(); body!.RootElement.GetProperty("error").GetProperty("code").GetString() .Should().Be("FEEDBACK_ALREADY_SUBMITTED"); } [Fact] public async Task SubmitFeedback_OpenAlert_Returns400() { using var scope = _fixture.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var openAlert = new ClinicalAlert { Id = Guid.NewGuid(), EncounterId = (await db.Encounters.FirstAsync()).Id, PatientId = (await db.Patients.FirstAsync()).Id, AlertType = AlertType.GcsWarning, Severity = AlertSeverity.Warning, Details = "Open alert.", Status = AlertStatus.Open, TriggeredAt = DateTimeOffset.UtcNow }; db.ClinicalAlerts.Add(openAlert); await db.SaveChangesAsync(); var resp = await _client.PostAsJsonAsync( $"/api/v1/alerts/{openAlert.Id}/feedback", new { feedbackType = "Useful" }); resp.StatusCode.Should().Be(HttpStatusCode.BadRequest); } [Fact] public async Task SubmitFeedback_WritesAuditLog() { await ResetAcknowledgedAlertAsync(); await _client.PostAsJsonAsync( $"/api/v1/alerts/{_alertId}/feedback", new { feedbackType = "Useful", comment = "audit check" }); using var scope = _fixture.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var logged = await db.ClinicalAuditLogs .AnyAsync(l => l.EntityId == _alertId && l.Action == AuditAction.AlertFeedbackSubmitted); logged.Should().BeTrue(); } [Fact] public async Task QualityMetricsSummary_ReturnsAggregate() { var resp = await _client.GetAsync("/api/v1/alerts/quality-metrics/summary"); resp.StatusCode.Should().Be(HttpStatusCode.OK); var body = await resp.Content.ReadFromJsonAsync(); body!.RootElement.GetProperty("data").GetProperty("totalAlerts").GetInt32() .Should().BeGreaterThanOrEqualTo(0); } [Fact] public async Task QualityMetrics_FilterByType() { var resp = await _client.GetAsync( "/api/v1/alerts/quality-metrics?alertType=NEWS2_WARNING"); resp.StatusCode.Should().Be(HttpStatusCode.OK); var body = await resp.Content.ReadFromJsonAsync(); body!.RootElement.GetProperty("data").GetProperty("items").GetArrayLength() .Should().BeGreaterThanOrEqualTo(0); } private async Task ResetAcknowledgedAlertAsync() { using var scope = _fixture.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var existing = await db.AlertFeedbacks .Where(f => f.AlertId == _alertId) .ToListAsync(); db.AlertFeedbacks.RemoveRange(existing); await db.SaveChangesAsync(); } }