using FluentAssertions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using StackExchange.Redis; [Collection("Integration")] public class GcsScoringTests : IAsyncLifetime { private readonly ApiFixture _fixture; private Guid _encounterId; private Guid _patientId; public GcsScoringTests(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-GCS-001", FirstName = "GCS", 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. GCS", AdmittedAt = DateTimeOffset.UtcNow, CreatedAt = DateTimeOffset.UtcNow }; db.Patients.Add(patient); db.Encounters.Add(encounter); await db.SaveChangesAsync(); _patientId = patient.Id; _encounterId = encounter.Id; var redis = scope.ServiceProvider.GetRequiredService(); var cache = redis.GetDatabase(); foreach (var key in GcsCalculator.AllComponentKeys(_encounterId)) await cache.KeyDeleteAsync(key); } public Task DisposeAsync() => Task.CompletedTask; private async Task ScoreAsync(string code, decimal value) { using var scope = _fixture.Services.CreateScope(); var detector = scope.ServiceProvider.GetRequiredService(); return await detector.ProcessObservationAsync(_encounterId, _patientId, code, value); } [Fact] public async Task IngestThreeComponents_ComputesTotal() { await ScoreAsync("GCS_EYE", 4m); await ScoreAsync("GCS_VERBAL", 5m); var result = await ScoreAsync("GCS_MOTOR", 6m); result.Outcome.Should().Be(GcsOutcome.ScoreComputed); result.TotalScore.Should().Be(15); result.Classification.Should().Be("MILD"); using var scope = _fixture.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var score = await db.GcsScores.SingleAsync(); score.TotalScore.Should().Be(15); } [Fact] public async Task GcsTotal8_CreatesCriticalAlert() { await ScoreAsync("GCS_EYE", 2m); await ScoreAsync("GCS_VERBAL", 3m); var result = await ScoreAsync("GCS_MOTOR", 3m); result.TotalScore.Should().Be(8); result.AlertCreated.Should().BeTrue(); using var scope = _fixture.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var alert = await db.ClinicalAlerts.SingleAsync(); alert.AlertType.Should().Be(AlertType.GcsCritical); alert.Severity.Should().Be(AlertSeverity.Critical); alert.AlertType.IsSuppressible().Should().BeFalse(); } [Fact] public async Task GcsTotal12_CreatesWarningAlert() { await ScoreAsync("GCS_EYE", 3m); await ScoreAsync("GCS_VERBAL", 4m); var result = await ScoreAsync("GCS_MOTOR", 5m); result.TotalScore.Should().Be(12); result.AlertCreated.Should().BeTrue(); using var scope = _fixture.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var alert = await db.ClinicalAlerts.SingleAsync(); alert.AlertType.Should().Be(AlertType.GcsWarning); alert.Severity.Should().Be(AlertSeverity.Warning); alert.AlertType.IsSuppressible().Should().BeTrue(); } [Fact] public async Task GcsTotal15_NoAlert() { await ScoreAsync("GCS_EYE", 4m); await ScoreAsync("GCS_VERBAL", 5m); await ScoreAsync("GCS_MOTOR", 6m); using var scope = _fixture.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); (await db.ClinicalAlerts.CountAsync()).Should().Be(0); } [Fact] public async Task PartialComponents_NoScore() { await ScoreAsync("GCS_EYE", 4m); await ScoreAsync("GCS_VERBAL", 5m); using var scope = _fixture.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); (await db.GcsScores.CountAsync()).Should().Be(0); } [Fact] public async Task GcsTriggersNews2Rescore() { using var scope = _fixture.Services.CreateScope(); var news2 = scope.ServiceProvider.GetRequiredService(); await news2.ProcessObservationAsync(_encounterId, _patientId, "RESP_RATE", 16m); await news2.ProcessObservationAsync(_encounterId, _patientId, "SPO2", 98m); await news2.ProcessObservationAsync(_encounterId, _patientId, "SYSTOLIC_BP", 120m); await news2.ProcessObservationAsync(_encounterId, _patientId, "HEART_RATE", 72m); await news2.ProcessObservationAsync(_encounterId, _patientId, "TEMP_C", 36.8m); await news2.ProcessObservationAsync(_encounterId, _patientId, "SUPPLEMENTAL_O2", 0m); await ScoreAsync("GCS_EYE", 2m); await ScoreAsync("GCS_VERBAL", 3m); await ScoreAsync("GCS_MOTOR", 4m); var result = await news2.ProcessObservationAsync(_encounterId, _patientId, "GCS_MOTOR", 4m); result.Outcome.Should().Be(News2Outcome.ScoreComputed); var db = scope.ServiceProvider.GetRequiredService(); var score = await db.News2Scores.OrderByDescending(s => s.CalculatedAt).FirstAsync(); score.ConsciousnessScore.Should().Be(3); } [Fact] public async Task GcsTriggersQsofaReeval() { using var scope = _fixture.Services.CreateScope(); var qsofa = scope.ServiceProvider.GetRequiredService(); await qsofa.ProcessObservationAsync(_encounterId, _patientId, "RESP_RATE", 24m); await ScoreAsync("GCS_EYE", 2m); await ScoreAsync("GCS_VERBAL", 3m); await ScoreAsync("GCS_MOTOR", 4m); var redis = scope.ServiceProvider.GetRequiredService(); var avpuKey = QsofaCalculator.CriterionKey(_encounterId, "AVPU"); var exists = await redis.GetDatabase().KeyExistsAsync(avpuKey); exists.Should().BeTrue("GCS total 9 should set altered mentation criterion"); } [Fact] public async Task AvpuFallback_WhenNoGcs() { using var scope = _fixture.Services.CreateScope(); var news2 = scope.ServiceProvider.GetRequiredService(); await news2.ProcessObservationAsync(_encounterId, _patientId, "RESP_RATE", 16m); await news2.ProcessObservationAsync(_encounterId, _patientId, "SPO2", 98m); await news2.ProcessObservationAsync(_encounterId, _patientId, "SYSTOLIC_BP", 120m); await news2.ProcessObservationAsync(_encounterId, _patientId, "HEART_RATE", 72m); await news2.ProcessObservationAsync(_encounterId, _patientId, "AVPU", 0m); await news2.ProcessObservationAsync(_encounterId, _patientId, "TEMP_C", 36.8m); var result = await news2.ProcessObservationAsync( _encounterId, _patientId, "SUPPLEMENTAL_O2", 0m); result.Outcome.Should().Be(News2Outcome.ScoreComputed); result.TotalScore.Should().Be(0); var db = scope.ServiceProvider.GetRequiredService(); var score = await db.News2Scores.SingleAsync(); score.ConsciousnessScore.Should().Be(0); } }