using FluentAssertions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using StackExchange.Redis; [Collection("Integration")] public class SofaScoringTests : IAsyncLifetime { private readonly ApiFixture _fixture; private Guid _encounterId; private Guid _patientId; public SofaScoringTests(ApiFixture fixture) => _fixture = fixture; 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-SOFA-001", FirstName = "SOFA", LastName = "Test", DateOfBirth = new DateOnly(1960, 1, 1), 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. SOFA", AdmittedAt = DateTimeOffset.UtcNow, CreatedAt = DateTimeOffset.UtcNow }; db.Patients.Add(patient); db.Encounters.Add(encounter); await db.SaveChangesAsync(); _patientId = patient.Id; _encounterId = encounter.Id; } public Task DisposeAsync() => Task.CompletedTask; private async Task ScoreObsAsync(string code, decimal value) { using var scope = _fixture.Services.CreateScope(); var detector = scope.ServiceProvider.GetRequiredService(); return await detector.ProcessObservationAsync( _encounterId, _patientId, code, value, DateTimeOffset.UtcNow); } [Fact] public async Task RespiratoryScore_PaO2FiO2Ratio() { await ScoreObsAsync("FIO2_PCT", 40m); var result = await ScoreObsAsync("PAO2_MMHG", 80m); result.Score!.Respiratory.Should().Be(2); } [Fact] public async Task RespiratoryScore_SpO2Fallback() { await ScoreObsAsync("FIO2_PCT", 40m); await ScoreObsAsync("SPO2", 94m); var result = await ScoreObsAsync("PLATELET_K_UL", 180m); result.Score!.Respiratory.Should().Be(1); } [Fact] public async Task CoagulationScore_LowPlatelets() { var result = await ScoreObsAsync("PLATELET_K_UL", 45m); result.Score!.Coagulation.Should().Be(3); } [Fact] public async Task LiverScore_ElevatedBilirubin() { var result = await ScoreObsAsync("BILIRUBIN_MG_DL", 3.5m); result.Score!.Liver.Should().Be(2); } [Fact] public async Task CardiovascularScore_LowMAP() { await ScoreObsAsync("SYSTOLIC_BP", 85m); var result = await ScoreObsAsync("DIASTOLIC_BP", 50m); result.Score!.Cardiovascular.Should().Be(1); } [Fact] public async Task CardiovascularScore_Vasopressor() { using var scope = _fixture.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var resolver = scope.ServiceProvider.GetRequiredService(); var detector = scope.ServiceProvider.GetRequiredService(); var med = new MedicationAdministration { Id = Guid.NewGuid(), EncounterId = _encounterId, DrugName = "NOREPINEPHRINE", Dose = 0.05m, DoseUnit = "mcg/kg/min", Route = "IV", AdministeredAt = DateTimeOffset.UtcNow, AdministeredBy = "RN Test" }; db.MedicationAdministrations.Add(med); await db.SaveChangesAsync(); await resolver.CacheFromAdministrationAsync(med, CancellationToken.None); var result = await detector.ProcessObservationAsync( _encounterId, _patientId, "SYSTOLIC_BP", 120m, DateTimeOffset.UtcNow); result.Score!.Cardiovascular.Should().BeGreaterThanOrEqualTo(2); } [Fact] public async Task CnsScore_FromGcs() { using var scope = _fixture.Services.CreateScope(); var gcs = scope.ServiceProvider.GetRequiredService(); await gcs.ProcessObservationAsync(_encounterId, _patientId, "GCS_EYE", 2m); await gcs.ProcessObservationAsync(_encounterId, _patientId, "GCS_VERBAL", 3m); await gcs.ProcessObservationAsync(_encounterId, _patientId, "GCS_MOTOR", 4m); var sofa = scope.ServiceProvider.GetRequiredService(); var result = await sofa.ProcessGcsScoredAsync(_encounterId, _patientId); result.Score!.Cns.Should().Be(3); } [Fact] public async Task RenalScore_ElevatedCreatinine() { var result = await ScoreObsAsync("CREATININE_MG_DL", 4.0m); result.Score!.Renal.Should().Be(3); } [Fact] public async Task BaselineEstablished_OnFirstCompleteScore() { await SeedBaselineInputsAsync(); using var scope = _fixture.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var baseline = await db.SofaScores.FirstAsync(s => s.IsBaseline); baseline.IsBaseline.Should().BeTrue(); } [Fact] public async Task DeltaTwo_CreatesSofaSepsisAlert() { await SeedBaselineInputsAsync(totalOffset: 0); await ScoreObsAsync("PLATELET_K_UL", 20m); await ScoreObsAsync("CREATININE_MG_DL", 4.5m); using var scope = _fixture.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var alert = await db.ClinicalAlerts.SingleAsync(a => a.AlertType == AlertType.SofaSepsis); alert.Severity.Should().Be(AlertSeverity.Critical); } [Fact] public async Task DeltaOne_CreatesWarning() { await SeedBaselineInputsAsync(totalOffset: 0); await ScoreObsAsync("PLATELET_K_UL", 120m); using var scope = _fixture.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var alert = await db.ClinicalAlerts.SingleAsync(a => a.AlertType == AlertType.SofaWarning); alert.Severity.Should().Be(AlertSeverity.Warning); } [Fact] public async Task NoDelta_NoAlert() { await SeedBaselineInputsAsync(); using var scope = _fixture.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); (await db.ClinicalAlerts.CountAsync()).Should().Be(0); } [Fact] public async Task CarryForward_WithinWindow() { using var scope = _fixture.Services.CreateScope(); var cache = scope.ServiceProvider.GetRequiredService(); await cache.StoreAsync(_encounterId, "PLATELET_K_UL", 180m, DateTimeOffset.UtcNow.AddHours(-8)); var result = await ScoreObsAsync("FIO2_PCT", 40m); result.Score!.Coagulation.Should().Be(0); } private async Task SeedBaselineInputsAsync(int totalOffset = 0) { await ScoreObsAsync("PAO2_MMHG", 100m); await ScoreObsAsync("FIO2_PCT", 40m); await ScoreObsAsync("PLATELET_K_UL", 180m - totalOffset); await ScoreObsAsync("BILIRUBIN_MG_DL", 1.0m); await ScoreObsAsync("SYSTOLIC_BP", 120m); await ScoreObsAsync("DIASTOLIC_BP", 80m); await ScoreObsAsync("CREATININE_MG_DL", 1.0m); await ScoreObsAsync("URINE_OUTPUT_ML_H", 50m); } }