using FluentAssertions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using StackExchange.Redis; using System.Net; using System.Net.Http.Json; using System.Text.Json; [Collection("Integration")] public class GcsScoringTests : IAsyncLifetime { private readonly ApiFixture _fixture; private readonly HttpClient _client; private Guid _encounterId; private Guid _patientId; public GcsScoringTests(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-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); } [Fact] public async Task GcsHistory_ReturnsPaginatedScores() { using var scope = _fixture.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var now = DateTimeOffset.UtcNow; db.GcsScores.AddRange( new GcsScore { Id = Guid.NewGuid(), EncounterId = _encounterId, PatientId = _patientId, EyeScore = 4, VerbalScore = 5, MotorScore = 6, TotalScore = 15, Classification = "MILD", CalculatedAt = now.AddHours(-2), CreatedAt = now, }, new GcsScore { Id = Guid.NewGuid(), EncounterId = _encounterId, PatientId = _patientId, EyeScore = 2, VerbalScore = 3, MotorScore = 3, TotalScore = 8, Classification = "SEVERE", CalculatedAt = now, CreatedAt = now, }); await db.SaveChangesAsync(); var resp = await _client.GetAsync( $"/api/v1/encounters/{_encounterId}/gcs/history?limit=10"); resp.StatusCode.Should().Be(HttpStatusCode.OK); var body = await resp.Content.ReadFromJsonAsync(); var items = body.GetProperty("data").GetProperty("items"); items.GetArrayLength().Should().Be(2); var latest = items[0]; latest.GetProperty("totalScore").GetInt32().Should().Be(8); latest.GetProperty("eyeScore").GetInt32().Should().Be(2); latest.GetProperty("verbalScore").GetInt32().Should().Be(3); latest.GetProperty("motorScore").GetInt32().Should().Be(3); var earliest = items[1]; earliest.GetProperty("totalScore").GetInt32().Should().Be(15); } }