using QuestionsApi.Models; using QuestionsApi.Services; using System.Text.Json; using MongoDB.Bson; using Xunit; namespace QuestionsApi.Tests; public sealed class CcsCaseTests { [Fact] public void NewCcsAttemptsAreTimedOnly() { Assert.Null(CcsCaseValidator.ValidateCreate(new CreateCcsAttemptRequest())); Assert.Equal("new CCS attempts must be timed", CcsCaseValidator.ValidateCreate(new CreateCcsAttemptRequest { Mode = CcsAttemptModes.Practice })); } [Fact] public void CcsBatchCommandsAreBoundedAndHomogeneous() { var valid = new CcsBatchCommandRequest { CommandId = "batch-1", Commands = [ new() { Type = "exam", Code = "general" }, new() { Type = "exam", Code = "cardiac" } ] }; Assert.Null(CcsCaseValidator.ValidateBatch(valid)); valid.Commands.Add(new() { Type = "order", Code = "bmp" }); Assert.Contains("cannot be mixed", CcsCaseValidator.ValidateBatch(valid), StringComparison.OrdinalIgnoreCase); } [Fact] public void CcsTimingReservesTwoMinutesWithoutExtendingTheHardDeadline() { Assert.Equal(18, CcsStore.ActivePhaseMinutes(20)); Assert.Equal(8, CcsStore.ActivePhaseMinutes(10)); var now = new DateTime(2026, 7, 23, 12, 0, 0, DateTimeKind.Utc); Assert.Equal(now.AddMinutes(2), CcsStore.FinalOrdersDeadline(now, now.AddMinutes(5))); Assert.Equal(now.AddMinutes(1), CcsStore.FinalOrdersDeadline(now, now.AddMinutes(1))); } [Fact] public void CcsHistoryCursorRoundTripsTimestampAndId() { var createdUtc = new DateTime(2026, 7, 19, 12, 34, 56, DateTimeKind.Utc); var id = ObjectId.GenerateNewId().ToString(); var encoded = CcsStore.EncodeHistoryCursor(createdUtc, id); Assert.True(CcsStore.TryDecodeHistoryCursor(encoded, out var decoded)); Assert.Equal(createdUtc, decoded!.CreatedUtc); Assert.Equal(id, decoded.Id); Assert.False(CcsStore.TryDecodeHistoryCursor("not-a-cursor", out _)); } [Fact] public void AppFeedbackValidationAcceptsSafeDiagnostics() { var request = new CreateAppFeedbackRequest { Category = "bug", Details = "The next button stopped responding.", Context = new AppFeedbackContext { Screen = "player", Platform = "Web", ViewportWidth = 1280, ViewportHeight = 720 } }; Assert.Null(AppFeedbackValidator.Validate(request)); request.Category = "content_accuracy"; Assert.Equal("category is invalid", AppFeedbackValidator.Validate(request)); request.Category = "bug"; request.Details = "short"; Assert.Contains("details", AppFeedbackValidator.Validate(request)); } [Fact] public void CcsReportCategoriesAreSpecificAndStable() { Assert.Contains("case_progression_scoring", CcsCaseReportCategory.All); Assert.DoesNotContain("answer_explanation", CcsCaseReportCategory.All); Assert.Equal(6, CcsCaseReportCategory.All.Count); } [Fact] public void DkaImportArtifactMatchesTheCaseSchema() { var path = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "../../../../../content/ccs/dka-emergency-001.json")); var request = JsonSerializer.Deserialize(File.ReadAllText(path), new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); Assert.NotNull(request); Assert.Empty(CcsCaseValidator.ValidateImport(request!, new HashSet())); Assert.True(request!.Cases.Single().ScoringCriteria.Count >= 12); } [Fact] public void PublishedCaseRequiresReferencesAndKnownOrders() { var request = ValidImport(); request.Cases[0].References = []; request.Cases[0].OrderResponses.Add(new CcsCaseOrderDefinition { OrderCode = "unknown" }); var errors = CcsCaseValidator.ValidateImport(request, new HashSet()); Assert.Contains(errors, value => value.Contains("reference", StringComparison.OrdinalIgnoreCase)); Assert.Contains(errors, value => value.Contains("unknown order code unknown")); } [Fact] public void ScoringCreditsTimelySequencedCareAndPenalizesHarm() { var definition = ValidImport().Cases[0]; definition.ScoringCriteria = [ new() { Id = "lab", Title = "Check electrolytes", ActionType = "order", TargetCode = "bmp", Points = 5, LatestMinute = 10, Rationale = "Check potassium." }, new() { Id = "insulin", Title = "Start insulin safely", ActionType = "order", TargetCode = "insulin", AfterTargetCode = "bmp", Points = 10, Rationale = "Start after potassium assessment." }, new() { Id = "bicarb", Title = "Avoid bicarbonate", ActionType = "order", TargetCode = "bicarb", Points = -4, Rationale = "Avoid routine bicarbonate." } ]; var attempt = new CcsAttempt { Actions = [ new() { Type = "order", Code = "bmp", SimulatedMinute = 2 }, new() { Type = "order", Code = "insulin", SimulatedMinute = 4 }, new() { Type = "order", Code = "bicarb", SimulatedMinute = 5 } ]}; var score = CcsStore.CalculateScore(attempt, definition); Assert.Equal(11, score.EarnedPoints); Assert.Equal(15, score.PossiblePoints); Assert.Equal(73.3, score.Percent); Assert.Contains(score.Criteria, value => value.CriterionId == "bicarb" && value.Status == "harmful"); } [Fact] public void ValidVersionedCasePassesValidation() { Assert.Empty(CcsCaseValidator.ValidateImport(ValidImport(), new HashSet())); } [Fact] public void CcsExpectedScoreUsesSeparateUserAndCaseElo() { Assert.Equal(0.5, CcsRatingStore.ExpectedScore(1000, 1000), 6); Assert.True(CcsRatingStore.ExpectedScore(1200, 1000) > 0.75); Assert.True(CcsRatingStore.ExpectedScore(800, 1000) < 0.25); } [Fact] public void ScoringReviewIncludesExpectedAndObservedEvidence() { var definition = ValidImport().Cases[0]; var attempt = new CcsAttempt { Actions = [new() { Type = "order", Code = "bmp", SimulatedMinute = 4 }] }; var criterion = CcsStore.CalculateScore(attempt, definition).Criteria.Single(); Assert.Equal("Check electrolytes", criterion.Title); Assert.Contains("Bmp", criterion.ExpectedAction, StringComparison.OrdinalIgnoreCase); Assert.Equal("bmp", criterion.ObservedAction); Assert.Equal(4, criterion.ObservedMinute); } private static CcsImportRequest ValidImport() => new() { Orders = [ new() { Code = "bmp", CanonicalName = "Basic metabolic panel", Category = "laboratory" }, new() { Code = "insulin", CanonicalName = "Insulin infusion", Category = "medication" }, new() { Code = "bicarb", CanonicalName = "Sodium bicarbonate", Category = "medication" } ], Cases = [ new() { ExternalId = "ccs-test-dka-001", Title = "Diabetic ketoacidosis", System = "Endocrine System", PresentingLocation = "emergency_department", AllowedLocations = ["emergency_department", "icu"], IsPublished = true, RealTimeMinutes = 20, Difficulty = 2, InitialChart = new() { PatientSummary = "Adult with diabetes", ReasonForEncounter = "Vomiting" }, OrderResponses = [new() { OrderCode = "bmp", Result = "Potassium 4.9 mEq/L" }], ScoringCriteria = [new() { Id = "lab", Title = "Check electrolytes", ActionType = "order", TargetCode = "bmp", Points = 5 }], References = [new() { Title = "Consensus report", Publisher = "Example Society", Url = "https://example.org/consensus", Supports = "Case management and scoring", AccessedOn = "2026-07-15", Reviewer = "Question bank editorial review" }] } ] }; }