using FluentAssertions; using Strata.ContinuousImprovement.Biz.Exploration; using Strata.ContinuousImprovement.Biz.Exploration.CaseTypeFamilies; using Strata.ContinuousImprovement.Biz.Exploration.Filters; using Strata.ContinuousImprovement.Biz.Test.Unit.Utilities; using System.Diagnostics.CodeAnalysis; namespace Strata.ContinuousImprovement.Biz.Test.Integration.SnowflakeIntegrationTests { [ExcludeFromCodeCoverage] [TestFixture, Category("Integration"), Category("CCI")] public class FlexibleExplorationSummaryTest : IntegrationTestBase { private static readonly Guid _configurationGuid = Guid.Parse("aa4cba4f-2506-462c-8e34-c45b21653988"); private static int _totalCaseTypeFamilyCount; [OneTimeSetUp] public async Task SetupExploration() { var explorationInfo = CreateExplorationInfo(); var summaries = await ExplorationService.GetCaseTypeFamilySummariesAsync(explorationInfo, CancellationToken.None); if (!summaries.Any()) throw new InvalidOperationException("No Case Type Family summaries returned for setup."); _totalCaseTypeFamilyCount = summaries.Count(); } [Test] public async Task GetCaseTypeFamilySummaries_WhenNoFilters_ReturnsAll() { // arrange var explorationInfo = CreateExplorationInfo(); // act var summaries = await ExplorationService.GetCaseTypeFamilySummariesAsync(explorationInfo, CancellationToken.None); // assert summaries.Should().HaveCount(_totalCaseTypeFamilyCount); await VerifySummaries(summaries); } [TestCaseSource(nameof(ExclusionCriteriaTestCases))] public async Task GetCaseTypeFamilySummaries_WithExclusions(ExclusionCriteria exclusions) { await RunExclusionSummaryTests(exclusions); } [Test] public async Task GetCaseTypeFamilySummaries_WithFilters_ReturnsFiltered() { // arrange - filtering on mortality (IsSelected = true => MortalityRate = 1) var booleanFilter = CreateBooleanFilter(ChipKey.Mortality); var explorationInfo = CreateExplorationInfo(booleanFilter); // act var summaries = await ExplorationService.GetCaseTypeFamilySummariesAsync(explorationInfo, CancellationToken.None); // assert summaries.Should().HaveCountGreaterThan(0); summaries.Should().HaveCountLessThan(_totalCaseTypeFamilyCount, "Filters should reduce data returned"); // mortality rate only applies to included encounters summaries.Where(x => x.Data.IncludedEncounterCount > 0).Should().OnlyContain(s => s.Data.MortalityRate == 1); await VerifySummaries(summaries); } [Test] public async Task GetCaseTypeFamilySummaries_WithFiltersAndExclusions_ReturnsFilteredAndExcluded() { // arrange - iqr + mad exclusion combo with a boolean filter var booleanFilter = CreateBooleanFilter(ChipKey.Mortality); var exclusions = new ExclusionCriteria { CostExclusionType = ExclusionCriteriaType.Iqr, CostExclusionValue = 1.5, LosExclusionType = ExclusionCriteriaType.Mad, LosExclusionValue = 2 }; var explorationInfo = CreateExplorationInfo(booleanFilter, exclusions); // act var summaries = await ExplorationService.GetCaseTypeFamilySummariesAsync(explorationInfo, CancellationToken.None); // assert summaries.Should().HaveCountGreaterThan(0); summaries.Should().HaveCountLessThan(_totalCaseTypeFamilyCount); // mortality rate only applies to included encounters summaries.Where(x => x.Data.IncludedEncounterCount > 0).Should().OnlyContain(s => s.Data.MortalityRate == 1); summaries.Should().Contain(s => s.Data.ExcludedEncountersAboveMedian > 0); await VerifySummaries(summaries); } private FilterChipItem CreateDischargeDateFilter() => new() { ChipType = ChipType.DateRange, Filters = [], DateRange = [new DateTime(2024, 1, 1), new DateTime(2025, 1, 1)] }; private FilterChipItem CreateBooleanFilter(ChipKey key) { if (key is ChipKey.Mortality) { return new FilterChipItem { ChipType = ChipType.Boolean, GlobalId = "Mortality", IsSelected = true, // determines true/false of filter Key = ChipKey.Mortality, Operator = OperatorType.In, Filters = new List() { new TreeNode { DisplayText = "Yes" }}, }; } throw new ArgumentException($"Unsupported boolean filter: {key}", nameof(key)); } private ExplorationInfo CreateExplorationInfo(FilterChipItem? additionalFilter = null, ExclusionCriteria? exclusion = null) { var dischargeDateFilter = CreateDischargeDateFilter(); var filterChipItems = additionalFilter != null ? new List { dischargeDateFilter, additionalFilter } : new List { dischargeDateFilter }; return new ExplorationInfo { ConfigurationGuid = _configurationGuid, ExclusionCriteria = exclusion ?? new ExclusionCriteria(), Filters = new Biz.Exploration.Filters.Filter { DischargeDateStart = dischargeDateFilter.DateRange.First(), DischargeDateEnd = dischargeDateFilter.DateRange.Last(), FilterChipItems = filterChipItems } }; } private async Task AssertExclusionsWereApplied(IEnumerable summaries) { summaries.Should().HaveCount(_totalCaseTypeFamilyCount); summaries.Should().Contain(s => s.Data.ExcludedEncountersAboveMedian > 0); } private Task VerifySummaries(IEnumerable summaries) { summaries.Should().NotBeNull().And.NotBeEmpty(); summaries.Should().OnlyContain(s => !string.IsNullOrWhiteSpace(s.Key)); summaries.Should().OnlyContain(s => !string.IsNullOrWhiteSpace(s.Name)); summaries.Should().AllSatisfy(s => s.Key.Should().MatchRegex(@"^\d+-\d+$", "Key should follow format 'level-id'")); summaries.Should().AllSatisfy(s => s.Data.Should().NotBeNull("Each summary should have data")); summaries.Should().AllSatisfy(s => s.Data.Key.Should().MatchRegex(@"^\d+-\d+$", "Key should follow format 'level-id'")); summaries.Should().AllSatisfy(s => s.Data.Id.Should().BeGreaterThan(0, "Each summary data should have a valid ID")); summaries.Should().AllSatisfy(s => s.Data.TotalEncounters.Should().BeGreaterThanOrEqualTo(0, "Each summary data should have a valid TotalEncounters")); summaries.Should().AllSatisfy(s => s.Data.TotalVariationEncounters.Should().BeGreaterThanOrEqualTo(0, "Each summary data should have a valid TotalVariationEncounters")); summaries.Should().AllSatisfy(s => s.Data.IncludedEncounterCount.Should().BeGreaterThanOrEqualTo(0, "Each summary data should have a valid IncludedEncounterCount")); summaries.Should().AllSatisfy(s => s.Data.IdentifiedVariation.Should().BeGreaterThanOrEqualTo(0, "Each summary data should have a valid IdentifiedVariation")); summaries.Should().AllSatisfy(s => s.Data.MortalityRate.Should().BeInRange(0, 1, "MortalityRate should be between 0 and 1")); summaries.Should().AllSatisfy(s => s.Data.MedianLos.Should().BeGreaterOrEqualTo(0, "MedianLos should be non-negative")); summaries.Should().AllSatisfy(s => s.Data.MedianCost.Should().BeGreaterOrEqualTo(0, "MedianCost should be non-negative")); summaries.Should().AllSatisfy(s => s.Data.IncludedEncountersBelowMedian.Should().BeGreaterOrEqualTo(0, "IncludedEncountersBelowMedian should be non-negative")); summaries.Should().AllSatisfy(s => s.Data.IncludedEncountersAboveMedian.Should().BeGreaterOrEqualTo(0, "IncludedEncountersAboveMedian should be non-negative")); summaries.Should().AllSatisfy(s => s.Data.IncludedVariation.Should().BeGreaterOrEqualTo(0, "IncludedVariation should be non-negative")); return Verify(summaries, verifySettings) .UseDirectory("Exploration") .UseFileName(TestContext.CurrentContext.TestName()); } private async Task RunExclusionSummaryTests(ExclusionCriteria exclusions) { // act var explorationInfo = CreateExplorationInfo(null, exclusions); var summaries = await ExplorationService.GetCaseTypeFamilySummariesAsync(explorationInfo, CancellationToken.None); // assert await AssertExclusionsWereApplied(summaries); await VerifySummaries(summaries); } private static IEnumerable ExclusionCriteriaTestCases() { yield return new TestCaseData( new ExclusionCriteria { CostExclusionType = ExclusionCriteriaType.Mad, CostExclusionValue = 2, LosExclusionType = ExclusionCriteriaType.Mad, LosExclusionValue = 2 } ).SetName("GetCaseTypeFamilySummaries_WithMADExclusions_ReturnsWithExclusions"); yield return new TestCaseData( new ExclusionCriteria { CostExclusionType = ExclusionCriteriaType.Iqr, CostExclusionValue = 1.5, LosExclusionType = ExclusionCriteriaType.Iqr, LosExclusionValue = 1.5 } ).SetName("GetCaseTypeFamilySummaries_WithIQRExclusions_ReturnsWithExclusions"); } } }