using System.Net; using System.Net.Http.Json; using System.Text.Json; using FluentAssertions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using StackExchange.Redis; [Collection("Integration")] public class ClinicalDemographicsAndObservationTests : IAsyncLifetime { private readonly ApiFixture _fixture; private readonly HttpClient _client; private Guid _patientId; private Guid _encounterId; public ClinicalDemographicsAndObservationTests(ApiFixture fixture) { _fixture = fixture; _client = fixture.CreateClient(); } 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-P10-001", FirstName = "Phase10", LastName = "Test", DateOfBirth = new DateOnly(1965, 3, 15), Gender = "F", BloodType = BloodType.BPositive, Allergies = "Sulfa", EmergencyContactName = "Test Contact", EmergencyContactPhone = "555-9999", CreatedAt = DateTimeOffset.UtcNow }; var encounter = new Encounter { Id = Guid.NewGuid(), PatientId = patient.Id, EncounterType = EncounterType.Inpatient, Status = EncounterStatus.Active, Department = Department.Icu, AttendingPhysician = "Dr. Phase10", RoomBed = "ICU-7A", AdmissionReason = "Sepsis workup", AdmittedAt = DateTimeOffset.UtcNow, CreatedAt = DateTimeOffset.UtcNow }; db.Patients.Add(patient); db.Encounters.Add(encounter); db.AlertThresholds.AddRange( new AlertThreshold { Id = Guid.NewGuid(), ObservationCode = "HEART_RATE", DisplayName = "Heart Rate", Unit = "bpm", CriticalLow = 30, WarningLow = 50, WarningHigh = 100, CriticalHigh = 150, CreatedAt = DateTimeOffset.UtcNow }, new AlertThreshold { Id = Guid.NewGuid(), ObservationCode = "SYSTOLIC_BP", DisplayName = "Systolic BP", Unit = "mmHg", CriticalLow = 70, WarningLow = 90, WarningHigh = 160, CriticalHigh = 180, CreatedAt = DateTimeOffset.UtcNow }, new AlertThreshold { Id = Guid.NewGuid(), ObservationCode = "DIASTOLIC_BP", DisplayName = "Diastolic BP", Unit = "mmHg", CriticalLow = 40, WarningLow = 60, WarningHigh = 90, CriticalHigh = 110, CreatedAt = DateTimeOffset.UtcNow }, new AlertThreshold { Id = Guid.NewGuid(), ObservationCode = "LACTATE_MMOL_L", DisplayName = "Serum Lactate", Unit = "mmol/L", CriticalLow = null, WarningLow = null, WarningHigh = 2.0m, CriticalHigh = 4.0m, CreatedAt = DateTimeOffset.UtcNow }, new AlertThreshold { Id = Guid.NewGuid(), ObservationCode = "AVPU", DisplayName = "AVPU Consciousness", Unit = "score", CriticalLow = null, WarningLow = null, WarningHigh = null, CriticalHigh = 2m, CreatedAt = DateTimeOffset.UtcNow }, new AlertThreshold { Id = Guid.NewGuid(), ObservationCode = "SUPPLEMENTAL_O2", DisplayName = "Supplemental O2", Unit = "flag", CriticalLow = null, WarningLow = null, WarningHigh = null, CriticalHigh = null, CreatedAt = DateTimeOffset.UtcNow }, new AlertThreshold { Id = Guid.NewGuid(), ObservationCode = "GLUCOSE_MG_DL", DisplayName = "Blood Glucose", Unit = "mg/dL", CriticalLow = 40, WarningLow = 70, WarningHigh = 180, CriticalHigh = 400, CreatedAt = DateTimeOffset.UtcNow } ); await db.SaveChangesAsync(); var redis = scope.ServiceProvider.GetRequiredService(); var cache = redis.GetDatabase(1); foreach (var t in await db.AlertThresholds.ToListAsync()) { var json = JsonSerializer.Serialize(new { t.ObservationCode, t.CriticalLow, t.WarningLow, t.WarningHigh, t.CriticalHigh }); await cache.StringSetAsync($"threshold:{t.ObservationCode}", json); } _patientId = patient.Id; _encounterId = encounter.Id; } public Task DisposeAsync() => Task.CompletedTask; [Fact] public async Task CriticalSystolicBp_AlertCreated() { var resp = await _client.PostAsJsonAsync( $"/api/v1/encounters/{_encounterId}/observations", new BatchIngestRequest(new List { new("SYSTOLIC_BP", 65, "mmHg", ObservationSource.Device, DateTimeOffset.UtcNow, null) })); resp.StatusCode.Should().Be(HttpStatusCode.Created); var body = await resp.Content.ReadFromJsonAsync(); body!.RootElement.GetProperty("data").GetProperty("alertGenerated").GetBoolean() .Should().BeTrue(); using var scope = _fixture.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var alert = await db.ClinicalAlerts.SingleAsync(); alert.AlertType.Should().Be(AlertType.CriticalSystolicBp); alert.Severity.Should().Be(AlertSeverity.Critical); } [Fact] public async Task AvpuUnresponsive_CriticalAlert() { var resp = await _client.PostAsJsonAsync( $"/api/v1/encounters/{_encounterId}/observations", new BatchIngestRequest(new List { new("AVPU", 3, "score", ObservationSource.Manual, DateTimeOffset.UtcNow, null) })); resp.StatusCode.Should().Be(HttpStatusCode.Created); using var scope = _fixture.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var alert = await db.ClinicalAlerts.SingleAsync(); alert.AlertType.Should().Be(AlertType.CriticalAvpu); } [Fact] public async Task SupplementalO2_NoAlert() { var resp = await _client.PostAsJsonAsync( $"/api/v1/encounters/{_encounterId}/observations", new BatchIngestRequest(new List { new("SUPPLEMENTAL_O2", 1, "flag", ObservationSource.Manual, DateTimeOffset.UtcNow, null) })); resp.StatusCode.Should().Be(HttpStatusCode.Created); using var scope = _fixture.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); (await db.ClinicalAlerts.CountAsync()).Should().Be(0); } [Fact] public async Task CriticalGlucose_AlertCreated() { var resp = await _client.PostAsJsonAsync( $"/api/v1/encounters/{_encounterId}/observations", new BatchIngestRequest(new List { new("GLUCOSE_MG_DL", 30, "mg/dL", ObservationSource.Lab, DateTimeOffset.UtcNow, null) })); resp.StatusCode.Should().Be(HttpStatusCode.Created); using var scope = _fixture.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var alert = await db.ClinicalAlerts.SingleAsync(); alert.AlertType.Should().Be(AlertType.CriticalGlucoseMgDl); } [Fact] public async Task PatientRegistration_ClinicalFields_RoundTrip() { var resp = await _client.PostAsJsonAsync("/api/v1/patients", new { firstName = "Demo", lastName = "Patient", dateOfBirth = "1990-06-15", gender = "M", bloodType = "AB-", allergies = "Latex, Iodine", emergencyContactName = "Demo Contact", emergencyContactPhone = "555-1234" }); resp.StatusCode.Should().Be(HttpStatusCode.Created); var body = await resp.Content.ReadFromJsonAsync(); var data = body!.RootElement.GetProperty("data"); data.GetProperty("bloodType").GetString().Should().Be("AB-"); data.GetProperty("allergies").GetString().Should().Be("Latex, Iodine"); data.GetProperty("emergencyContactName").GetString().Should().Be("Demo Contact"); } [Fact] public async Task OpenEncounter_WithRoomBed_Success() { var patientResp = await _client.PostAsJsonAsync("/api/v1/patients", new { firstName = "Room", lastName = "Test", dateOfBirth = "1985-01-01", gender = "F" }); var patientBody = await patientResp.Content.ReadFromJsonAsync(); var newPatientId = patientBody!.RootElement.GetProperty("data").GetProperty("id").GetGuid(); var resp = await _client.PostAsJsonAsync($"/api/v1/patients/{newPatientId}/encounters", new { encounterType = "INPATIENT", department = "ICU", attendingPhysician = "Dr. Room", roomBed = "ICU-3C", admissionReason = "Acute respiratory distress" }); resp.StatusCode.Should().Be(HttpStatusCode.Created); var body = await resp.Content.ReadFromJsonAsync(); var data = body!.RootElement.GetProperty("data"); data.GetProperty("roomBed").GetString().Should().Be("ICU-3C"); data.GetProperty("admissionReason").GetString().Should().Be("Acute respiratory distress"); } }