using QuestionsApi.Models; using QuestionsApi.Services; using Xunit; namespace QuestionsApi.Tests; public sealed class DashboardTests { [Fact] public void PercentageRoundsToNearestWholeNumber() { Assert.Equal(67, DashboardStore.Percentage(2, 3)); Assert.Equal(0, DashboardStore.Percentage(0, 0)); } [Fact] public void StreakIncludesYesterdayWhenUserHasNotStudiedToday() { var today = new DateTime(2026, 7, 18, 0, 0, 0, DateTimeKind.Utc); var dates = new[] { today.AddDays(-1), today.AddDays(-2), today.AddDays(-3), today.AddDays(-5) }; Assert.Equal(3, DashboardStore.CalculateCurrentStreak(dates, today)); } [Fact] public void StreakStopsAtFirstMissingDay() { var today = new DateTime(2026, 7, 18, 0, 0, 0, DateTimeKind.Utc); var dates = new[] { today, today.AddDays(-1), today.AddDays(-3) }; Assert.Equal(2, DashboardStore.CalculateCurrentStreak(dates, today)); } [Fact] public void SystemPerformanceReturnsEveryOfficialSystemInOrder() { var attempts = new[] { Attempt("cardio-1", true), Attempt("cardio-2", false), Attempt("ethics-1", true), Attempt("ignored", true), Attempt("skipped-cardio", false, true) }; var questions = new Dictionary { ["cardio-1"] = Question("Cardiology"), ["cardio-2"] = Question("Cardiovascular System"), ["ethics-1"] = Question("Medical Ethics"), ["ignored"] = Question("Unmapped Specialty"), ["skipped-cardio"] = Question("Cardiology") }; var result = DashboardStore.BuildSystemPerformance(attempts, questions); Assert.Equal(14, result.Count); Assert.Equal(DashboardStore.Step3Systems, result.Select(value => value.System)); var cardiovascular = result.Single(value => value.System == "Cardiovascular System"); Assert.Equal(1, cardiovascular.Correct); Assert.Equal(2, cardiovascular.Total); Assert.Equal(50, cardiovascular.Accuracy); var social = result.Single(value => value.System.StartsWith("Social Sciences")); Assert.Equal(1, social.Correct); Assert.Equal(1, social.Total); Assert.All(result.Where(value => value.System != cardiovascular.System && value.System != social.System), value => Assert.Equal(0, value.Total)); } [Theory] [InlineData("Preventive Medicine", "Human Development")] [InlineData("Infectious Disease", "Immune System, Blood & Lymphoreticular System, and Multisystem Processes/Disorders")] [InlineData("Dermatology", "Skin & Subcutaneous Tissue")] [InlineData("Pulmonary", "Respiratory System")] [InlineData("Pregnancy", "Pregnancy/Childbirth & Female Reproductive System & Breast")] [InlineData("Biostatistics", "Biostatistics & Epidemiology/Population Health & Interpretation of the Medical Literature")] [InlineData("Medical Ethics", "Social Sciences: Communication Skills/Ethics/Patient Safety")] [InlineData("Integumentary System", "Skin & Subcutaneous Tissue")] [InlineData("Eye & Adnexa", "Nervous System & Special Senses")] [InlineData("Toxicology", "Immune System, Blood & Lymphoreticular System, and Multisystem Processes/Disorders")] public void SystemAliasesMapToOfficialCategories(string source, string expected) { Assert.Equal(expected, DashboardStore.NormalizeStep3System(source)); } [Fact] public void TrendCarriesRatingForwardAndLeavesInactiveAccuracyAsGapData() { var today = new DateTime(2026, 7, 25, 0, 0, 0, DateTimeKind.Utc); var baselineDate = today.AddDays(-2).AddHours(8); var attempts = new[] { Attempt("one", true, createdUtc: today.AddDays(-2).AddHours(9), rating: 1018, ratingUtc: today.AddDays(-2).AddHours(9)), Attempt("two", false, createdUtc: today.AddDays(-2).AddHours(10), rating: 1002, ratingUtc: today.AddDays(-2).AddHours(10)), Attempt("three", true, createdUtc: today.AddHours(11), rating: 1020, ratingUtc: today.AddHours(11)), Attempt("skip", false, true, today.AddDays(-1).AddHours(4), 990, today.AddDays(-1).AddHours(4)) }; var rating = new UserRating { OverallElo = 1020, HistoryBaselineElo = 1000, HistoryStartedUtc = baselineDate }; var result = DashboardStore.BuildPerformanceTrend( attempts, rating, Array.Empty(), new CcsUserRating(), today); Assert.Equal(30, result.Count); Assert.Null(result[^4].QuestionBankRating); Assert.Equal(1002, result[^3].QuestionBankRating); Assert.Equal(50, result[^3].Accuracy); Assert.Equal(2, result[^3].Total); Assert.Equal(990, result[^2].QuestionBankRating); Assert.Equal(0, result[^2].Total); Assert.Equal(1020, result[^1].QuestionBankRating); Assert.Equal(100, result[^1].Accuracy); } [Fact] public void TrendCarriesCcsRatingForwardIndependentlyOfQuestionBankRating() { var today = new DateTime(2026, 7, 25, 0, 0, 0, DateTimeKind.Utc); var baselineDate = today.AddDays(-2).AddHours(8); var rating = new UserRating(); var ccsRating = new CcsUserRating { Elo = 1050, HistoryBaselineElo = 1000, HistoryStartedUtc = baselineDate }; var ccsAttempts = new[] { RatedCcsAttempt("one", today.AddDays(-1).AddHours(9), 1030), RatedCcsAttempt("two", today.AddHours(10), 1050) }; var result = DashboardStore.BuildPerformanceTrend( Array.Empty(), rating, ccsAttempts, ccsRating, today); Assert.Equal(30, result.Count); Assert.Equal(1000, result[^3].CcsRating); Assert.Equal(1030, result[^2].CcsRating); Assert.Equal(1050, result[^1].CcsRating); } private static CcsAttempt RatedCcsAttempt(string caseExternalId, DateTime appliedUtc, int userElo) => new() { CaseExternalId = caseExternalId, Rating = new CcsRatingChange { Rated = true, UserElo = userElo, AppliedUtc = appliedUtc } }; private static QuestionAttempt Attempt( string questionId, bool correct, bool skip = false, DateTime? createdUtc = null, int? rating = null, DateTime? ratingUtc = null) => new() { QuestionExternalId = questionId, IsCorrect = correct, IsSkip = skip, CreatedUtc = createdUtc ?? DateTime.UtcNow, UserOverallEloAfter = rating, RatingAppliedUtc = ratingUtc }; private static Question Question(string system) => new() { ExternalId = system, System = system }; [Fact] public void PercentileReturnsNullWithNoUsers() { Assert.Null(DashboardStore.Percentile(0, 0)); } [Fact] public void PercentileComputesTopPercentFromUsersRatedHigher() { // 18 of 100 users rate higher than this user - "top 18%" Assert.Equal(18, DashboardStore.Percentile(18, 100)); } [Fact] public void PercentileNeverReturnsZero() { // Even the single best-rated user should read as "top 1%", not "top 0%" Assert.Equal(1, DashboardStore.Percentile(0, 100)); } [Theory] [InlineData(0.85, "High")] [InlineData(0.7, "High")] [InlineData(0.55, "Medium")] [InlineData(0.4, "Medium")] [InlineData(0.1, "Low")] public void ReadinessLabelUsesPassProbabilityThresholds(double passProbability, string expected) { Assert.Equal(expected, DashboardStore.ReadinessLabel(passProbability)); } [Fact] public void SelectLatestReadinessScoreSkipsSampleSimulationsEvenWhenMostRecent() { var today = new DateTime(2026, 7, 25, 0, 0, 0, DateTimeKind.Utc); var simulations = new[] { CompletedSimulation(isSample: true, completedUtc: today, passProbability: 0.98), CompletedSimulation(isSample: false, completedUtc: today.AddDays(-3), passProbability: 0.12) }; var score = DashboardStore.SelectLatestReadinessScore(simulations); Assert.NotNull(score); Assert.Equal(0.12, score.PassProbability); } [Fact] public void SelectLatestReadinessScoreReturnsNullWhenOnlySamplesExist() { var simulations = new[] { CompletedSimulation(isSample: true, completedUtc: DateTime.UtcNow, passProbability: 0.98) }; Assert.Null(DashboardStore.SelectLatestReadinessScore(simulations)); } private static ExamSimulation CompletedSimulation(bool isSample, DateTime completedUtc, double passProbability) => new() { IsSample = isSample, CompletedUtc = completedUtc, Score = new ExamSimulationScore { PassProbability = passProbability } }; }