using FluentAssertions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using StackExchange.Redis; [Collection("Integration")] public class MedicationCorrelationTests : IAsyncLifetime { private readonly ApiFixture _fixture; private Guid _patientId; private Guid _encounterId; public MedicationCorrelationTests(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-CORR-001", FirstName = "Correlation", LastName = "Test", DateOfBirth = new DateOnly(1970, 6, 10), 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. Correlation", AdmittedAt = DateTimeOffset.UtcNow, CreatedAt = DateTimeOffset.UtcNow }; db.Patients.Add(patient); db.Encounters.Add(encounter); db.AlertThresholds.Add(new AlertThreshold { Id = Guid.NewGuid(), ObservationCode = "SYSTOLIC_BP", DisplayName = "Systolic BP", Unit = "mmHg", CriticalLow = 70, WarningLow = 90, WarningHigh = 180, CriticalHigh = 220, CreatedAt = DateTimeOffset.UtcNow }); db.AlertThresholds.Add(new AlertThreshold { Id = Guid.NewGuid(), ObservationCode = "RESP_RATE", DisplayName = "Respiratory Rate", Unit = "breaths/min", CriticalLow = 5, WarningLow = 8, WarningHigh = 25, CriticalHigh = 35, CreatedAt = DateTimeOffset.UtcNow }); await db.SaveChangesAsync(); var redis = scope.ServiceProvider.GetRequiredService(); var cache = redis.GetDatabase(1); await cache.StringSetAsync("threshold:SYSTOLIC_BP", """{"ObservationCode":"SYSTOLIC_BP","CriticalLow":70,"WarningLow":90,"WarningHigh":180,"CriticalHigh":220}"""); await cache.StringSetAsync("threshold:RESP_RATE", """{"ObservationCode":"RESP_RATE","CriticalLow":5,"WarningLow":8,"WarningHigh":25,"CriticalHigh":35}"""); _patientId = patient.Id; _encounterId = encounter.Id; } [Fact] public async Task MetoprololThenLowBp_AnnotatedDetails() { await ResetAndSeedAsync(); using var scope = _fixture.Services.CreateScope(); var medService = scope.ServiceProvider.GetRequiredService(); await medService.CreateAsync(_encounterId, new CreateMedicationAdministrationRequest( "metoprolol", 25m, "mg", "PO", DateTimeOffset.UtcNow.AddMinutes(-45), "nurse-1")); var evaluator = scope.ServiceProvider.GetRequiredService(); var created = await evaluator.EvaluateAsync( Guid.NewGuid(), _encounterId, _patientId, "SYSTOLIC_BP", 85m); created.Should().BeTrue(); var db = scope.ServiceProvider.GetRequiredService(); var alert = await db.ClinicalAlerts .SingleAsync(a => a.AlertType == AlertType.WarningSystolicBp); alert.Details.Should().Contain("metoprolol"); alert.Details.Should().Contain("min ago"); alert.Details.Should().Contain("note:"); } [Fact] public async Task LowBpWithoutMedication_Unannotated() { await ResetAndSeedAsync(); using var scope = _fixture.Services.CreateScope(); var evaluator = scope.ServiceProvider.GetRequiredService(); var created = await evaluator.EvaluateAsync( Guid.NewGuid(), _encounterId, _patientId, "SYSTOLIC_BP", 85m); created.Should().BeTrue(); var db = scope.ServiceProvider.GetRequiredService(); var alert = await db.ClinicalAlerts .SingleAsync(a => a.AlertType == AlertType.WarningSystolicBp); alert.Details.Should().NotContain("note:"); } [Fact] public async Task UnrelatedDrug_NoAnnotation() { await ResetAndSeedAsync(); using var scope = _fixture.Services.CreateScope(); var medService = scope.ServiceProvider.GetRequiredService(); // Insulin maps to GLUCOSE_MG_DL, not SYSTOLIC_BP await medService.CreateAsync(_encounterId, new CreateMedicationAdministrationRequest( "insulin", 10m, "units", "SubQ", DateTimeOffset.UtcNow.AddMinutes(-30), "nurse-1")); var evaluator = scope.ServiceProvider.GetRequiredService(); var created = await evaluator.EvaluateAsync( Guid.NewGuid(), _encounterId, _patientId, "SYSTOLIC_BP", 85m); created.Should().BeTrue(); var db = scope.ServiceProvider.GetRequiredService(); var alert = await db.ClinicalAlerts .SingleAsync(a => a.AlertType == AlertType.WarningSystolicBp); alert.Details.Should().NotContain("note:"); alert.Details.Should().NotContain("insulin"); } [Fact] public async Task MedicationOutsideWindow_NoAnnotation() { await ResetAndSeedAsync(); using var scope = _fixture.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); // Metoprolol administered 2 hours ago — outside 90-min window db.MedicationAdministrations.Add(new MedicationAdministration { Id = Guid.NewGuid(), EncounterId = _encounterId, DrugName = "metoprolol", Dose = 25m, DoseUnit = "mg", Route = "PO", AdministeredAt = DateTimeOffset.UtcNow.AddMinutes(-120), AdministeredBy = "nurse-1" }); await db.SaveChangesAsync(); var evaluator = scope.ServiceProvider.GetRequiredService(); var created = await evaluator.EvaluateAsync( Guid.NewGuid(), _encounterId, _patientId, "SYSTOLIC_BP", 85m); created.Should().BeTrue(); var alert = await db.ClinicalAlerts .SingleAsync(a => a.AlertType == AlertType.WarningSystolicBp); alert.Details.Should().NotContain("note:"); alert.Details.Should().NotContain("metoprolol"); } [Fact] public async Task MorphineThenLowRespRate_Annotated() { await ResetAndSeedAsync(); using var scope = _fixture.Services.CreateScope(); var medService = scope.ServiceProvider.GetRequiredService(); await medService.CreateAsync(_encounterId, new CreateMedicationAdministrationRequest( "morphine", 4m, "mg", "IV", DateTimeOffset.UtcNow.AddMinutes(-20), "nurse-1")); var evaluator = scope.ServiceProvider.GetRequiredService(); var created = await evaluator.EvaluateAsync( Guid.NewGuid(), _encounterId, _patientId, "RESP_RATE", 7m); created.Should().BeTrue(); var db = scope.ServiceProvider.GetRequiredService(); var alert = await db.ClinicalAlerts .SingleAsync(a => a.AlertType == AlertType.WarningRespRate); alert.Details.Should().Contain("morphine"); alert.Details.Should().Contain("min ago"); alert.Details.Should().Contain("note:"); } }