using System.Text.Json; using FluentAssertions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using StackExchange.Redis; [Collection("Integration")] public class WarningAlertTests : IAsyncLifetime { private readonly ApiFixture _fixture; private Guid _patientId; private Guid _encounterId; public WarningAlertTests(ApiFixture fixture) => _fixture = fixture; public async Task InitializeAsync() => await ResetAndSeedAsync(); public Task DisposeAsync() => Task.CompletedTask; private async Task ResetAndSeedAsync() { using var scope = _fixture.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); await DbResetHelper.ResetAsync(db); var patient = new Patient { Id = Guid.NewGuid(), Mrn = "MRN-WA-001", FirstName = "Warning", LastName = "Test", DateOfBirth = new DateOnly(1975, 5, 20), Gender = "M", CreatedAt = DateTimeOffset.UtcNow }; var encounter = new Encounter { Id = Guid.NewGuid(), PatientId = patient.Id, EncounterType = EncounterType.Inpatient, Status = EncounterStatus.Active, Department = Department.Icu, AttendingPhysician = "Dr. Warning", AdmittedAt = DateTimeOffset.UtcNow, CreatedAt = DateTimeOffset.UtcNow }; db.Patients.Add(patient); db.Encounters.Add(encounter); db.AlertThresholds.Add(new AlertThreshold { Id = Guid.NewGuid(), ObservationCode = "HEART_RATE", DisplayName = "Heart Rate", Unit = "bpm", CriticalLow = 30, WarningLow = 50, WarningHigh = 100, CriticalHigh = 150, CreatedAt = DateTimeOffset.UtcNow }); await db.SaveChangesAsync(); var redis = scope.ServiceProvider.GetRequiredService(); var cache = redis.GetDatabase(1); await cache.StringSetAsync("threshold:HEART_RATE", """{"ObservationCode":"HEART_RATE","CriticalLow":30,"WarningLow":50,"WarningHigh":100,"CriticalHigh":150}"""); _patientId = patient.Id; _encounterId = encounter.Id; } [Fact] public async Task WarningHeartRate_AlertCreated() { await ResetAndSeedAsync(); using var scope = _fixture.Services.CreateScope(); var evaluator = scope.ServiceProvider.GetRequiredService(); var created = await evaluator.EvaluateAsync( Guid.NewGuid(), _encounterId, _patientId, "HEART_RATE", 105m); created.Should().BeTrue(); var db = scope.ServiceProvider.GetRequiredService(); var alert = await db.ClinicalAlerts.SingleAsync(); alert.AlertType.Should().Be(AlertType.WarningHeartRate); alert.Severity.Should().Be(AlertSeverity.Warning); alert.Status.Should().Be(AlertStatus.Open); } [Fact] public async Task NormalHeartRate_NoAlert() { await ResetAndSeedAsync(); using var scope = _fixture.Services.CreateScope(); var evaluator = scope.ServiceProvider.GetRequiredService(); var created = await evaluator.EvaluateAsync( Guid.NewGuid(), _encounterId, _patientId, "HEART_RATE", 78m); created.Should().BeFalse(); var db = scope.ServiceProvider.GetRequiredService(); (await db.ClinicalAlerts.CountAsync()).Should().Be(0); } [Fact] public async Task CriticalHeartRate_NoWarningAlert() { await ResetAndSeedAsync(); using var scope = _fixture.Services.CreateScope(); var evaluator = scope.ServiceProvider.GetRequiredService(); var created = await evaluator.EvaluateAsync( Guid.NewGuid(), _encounterId, _patientId, "HEART_RATE", 160m); created.Should().BeFalse(); var db = scope.ServiceProvider.GetRequiredService(); (await db.ClinicalAlerts.CountAsync()).Should().Be(0); } [Fact] public async Task DuplicateWarning_Idempotent() { await ResetAndSeedAsync(); using var scope = _fixture.Services.CreateScope(); var evaluator = scope.ServiceProvider.GetRequiredService(); await evaluator.EvaluateAsync(Guid.NewGuid(), _encounterId, _patientId, "HEART_RATE", 105m); await evaluator.EvaluateAsync(Guid.NewGuid(), _encounterId, _patientId, "HEART_RATE", 108m); var db = scope.ServiceProvider.GetRequiredService(); (await db.ClinicalAlerts.CountAsync()).Should().Be(1, "second warning for same type must be idempotent while first is still open"); } }