using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Moq; using Strata.ContinuousImprovement.Biz.DbContexts; using Strata.ContinuousImprovement.Biz.Exploration; using Strata.ContinuousImprovement.Biz.Exploration.Filters; using Strata.ContinuousImprovement.Biz.FiscalMonths; using Strata.ContinuousImprovement.Biz.Test.Unit.Utilities; using Strata.ContinuousImprovement.Biz.Utilities; using Strata.Schema.Client; using Strata.Schema.Client.Dtos; using Strata.Schema.Client.Dtos.Info; using Strata.SqlTools.Configuration.Common.AsyncFactory; using Strata.StrataSphereCompare.Client; using System.Diagnostics.CodeAnalysis; using VerifyNUnit; namespace Strata.ContinuousImprovement.Biz.Test.Integration.SnowflakeIntegrationTests { /// /// Baseline regression tests for the exploration query refactor. /// /// WORKFLOW: /// 1. On main (or a branch from main): run these tests to generate golden snapshots. /// Commit the resulting *.verified.txt files. /// 2. On the refactor branch: run the same tests. /// Verify diffs output against the golden snapshots — any divergence fails. /// [ExcludeFromCodeCoverage] [TestFixture, Category("Integration"), Category("CCI")] public class FlexibleExplorationBaselineTest : IntegrationTestBase { private static readonly Guid _configurationGuid = Guid.Parse("aa4cba4f-2506-462c-8e34-c45b21653988"); private ExplorationDetailInfo _detailInfo; [OneTimeSetUp] public async Task SetupDetailInfo() { var summaryInfo = CreateSummaryInfo(); var summaries = await ExplorationService.GetCaseTypeFamilySummariesAsync(summaryInfo, CancellationToken.None); if (!summaries.Any()) throw new InvalidOperationException("No Case Type Family summaries returned — cannot run baseline tests."); // Configure the schema service mock so the Gender ScoreMember filter resolves to // a real SQL fragment (fpes.GenderID IN (1)) rather than being silently skipped. // GenderID 1 = Male in the GM Automation test database. var genderDimensionGuid = Guid.NewGuid(); Mock.Get(SchemaServiceClient) .Setup(s => s.GetDimensionByGlobalIdAsync("Gender", It.IsAny())) .ReturnsAsync(new ScoreDimensionInfoDto { DimensionGuid = genderDimensionGuid }); Mock.Get(SchemaServiceClient) .Setup(s => s.GetDimensionMembersFromHierarchyPathsAsync(genderDimensionGuid, It.IsAny>(), It.IsAny())) .ReturnsAsync(new[] { new MemberDto { Id = "1" } }); // Rebuild ExplorationFilterService and ExplorationService with the configured mock // so the gender filter is active for all tests in this fixture. var filterService = new ExplorationFilterService( ServiceProvider.GetRequiredService>(), SchemaServiceClient, Mock.Of>()); var featureFlagWrapper = Mock.Of(); Mock.Get(featureFlagWrapper) .Setup(f => f.IsCiBenchmarkingEnabled(It.IsAny())) .ReturnsAsync(true); ExplorationService = new ExplorationService( JazzConnBuilderFactory, SnowflakeDatabaseContext, Mock.Of(), featureFlagWrapper, Mock.Of(), filterService, Mock.Of>()); // Always pick the same CTF (highest encounter count) so snapshots are deterministic. var busiest = summaries.OrderByDescending(s => s.Data.TotalEncounters).First(); // Derive a real cost driver from the CTF's detail breakdown so the // encounter query (which filters on cc.costdriver = @CostDriver) returns data. var costDriverInfo = CreateSummaryInfo(); costDriverInfo.CaseTypeFamilyId = busiest.Data.Id; var costDriverDetails = await ExplorationService.GetCaseTypeFamilySummaryDetailsAsync(costDriverInfo, CancellationToken.None); var costDriver = costDriverDetails .SelectMany(d => d.Children ?? []) .OrderByDescending(c => c.Data.TotalEncounters) .FirstOrDefault()?.Name ?? throw new InvalidOperationException("No cost driver children found for the busiest CTF."); _detailInfo = new ExplorationDetailInfo { ConfigurationGuid = _configurationGuid, CaseTypeFamilyId = busiest.Data.Id, CostDriver = costDriver, ExclusionCriteria = new ExclusionCriteria(), ExcludedEncounters = new List(), IncludedEncounters = new List(), Filters = new Biz.Exploration.Filters.Filter { DischargeDateStart = new DateTime(2024, 1, 1), DischargeDateEnd = new DateTime(2025, 1, 1), FilterChipItems = new List { new FilterChipItem { ChipType = ChipType.DateRange, Key = ChipKey.DischargeDateID, Filters = [], DateRange = [new DateTime(2024, 1, 1), new DateTime(2025, 1, 1)] } } } }; } // ── No filters ────────────────────────────────────────────────────────── [Test] public async Task GetCaseTypeFamilyDetailsAsync_MatchesBaseline() { var result = await ExplorationService.GetCaseTypeFamilyDetailsAsync(_detailInfo, CancellationToken.None); await Verifier.Verify(result, verifySettings) .UseFileName(TestContext.CurrentContext.TestName()); } [Test] public async Task GetEncountersAsync_MatchesBaseline() { var result = await ExplorationService.GetEncountersAsync(_detailInfo, _configurationGuid, CancellationToken.None); await Verifier.Verify(result.OrderBy(e => e.EncounterId), verifySettings) .UseFileName(TestContext.CurrentContext.TestName()); } // ── Boolean filter (charges > 0) ───────────────────────────────────── // IsGreaterThanZeroCharges hits the queryFilter path and reliably // produces a meaningful subset without risking empty results. [Test] public async Task GetCaseTypeFamilyDetailsAsync_WithChargesFilter_MatchesBaseline() { var info = CreateDetailInfo(extraFilter: CreateChargesFilter()); var result = await ExplorationService.GetCaseTypeFamilyDetailsAsync(info, CancellationToken.None); await Verifier.Verify(result, verifySettings) .UseFileName(TestContext.CurrentContext.TestName()); } [Test] public async Task GetEncountersAsync_WithChargesFilter_MatchesBaseline() { var info = CreateDetailInfo(extraFilter: CreateChargesFilter()); var result = await ExplorationService.GetEncountersAsync(info, _configurationGuid, CancellationToken.None); await Verifier.Verify(result.OrderBy(e => e.EncounterId), verifySettings) .UseFileName(TestContext.CurrentContext.TestName()); } // ── FPES ScoreMember filter (gender) ──────────────────────────────── // Gender is an FPES-level filter — it resolves to fpes.GenderID IN (...) // via the schema service and lands in the encounterSummaryFilter placeholder. [Test] public async Task GetCaseTypeFamilyDetailsAsync_WithGenderFilter_MatchesBaseline() { var info = CreateDetailInfo(extraFilter: CreateGenderFilter()); var result = await ExplorationService.GetCaseTypeFamilyDetailsAsync(info, CancellationToken.None); await Verifier.Verify(result, verifySettings) .UseFileName(TestContext.CurrentContext.TestName()); } [Test] public async Task GetEncountersAsync_WithGenderFilter_MatchesBaseline() { var info = CreateDetailInfo(extraFilter: CreateGenderFilter()); var result = await ExplorationService.GetEncountersAsync(info, _configurationGuid, CancellationToken.None); await Verifier.Verify(result.OrderBy(e => e.EncounterId), verifySettings) .UseFileName(TestContext.CurrentContext.TestName()); } // ── IQR exclusions ─────────────────────────────────────────────────── [Test] public async Task GetCaseTypeFamilyDetailsAsync_WithIqrExclusions_MatchesBaseline() { var info = CreateDetailInfo(exclusion: new ExclusionCriteria { CostExclusionType = ExclusionCriteriaType.Iqr, CostExclusionValue = 1.5, LosExclusionType = ExclusionCriteriaType.Iqr, LosExclusionValue = 1.5 }); var result = await ExplorationService.GetCaseTypeFamilyDetailsAsync(info, CancellationToken.None); await Verifier.Verify(result, verifySettings) .UseFileName(TestContext.CurrentContext.TestName()); } [Test] public async Task GetEncountersAsync_WithIqrExclusions_MatchesBaseline() { var info = CreateDetailInfo(exclusion: new ExclusionCriteria { CostExclusionType = ExclusionCriteriaType.Iqr, CostExclusionValue = 1.5, LosExclusionType = ExclusionCriteriaType.Iqr, LosExclusionValue = 1.5 }); var result = await ExplorationService.GetEncountersAsync(info, _configurationGuid, CancellationToken.None); await Verifier.Verify(result.OrderBy(e => e.EncounterId), verifySettings) .UseFileName(TestContext.CurrentContext.TestName()); } // ── Helpers ────────────────────────────────────────────────────────── private ExplorationDetailInfo CreateDetailInfo( FilterChipItem extraFilter = null, ExclusionCriteria exclusion = null) { var dateFilter = new FilterChipItem { ChipType = ChipType.DateRange, Key = ChipKey.DischargeDateID, Filters = [], DateRange = [new DateTime(2024, 1, 1), new DateTime(2025, 1, 1)] }; var chips = extraFilter != null ? new List { dateFilter, extraFilter } : new List { dateFilter }; return new ExplorationDetailInfo { ConfigurationGuid = _detailInfo.ConfigurationGuid, CaseTypeFamilyId = _detailInfo.CaseTypeFamilyId, CostDriver = _detailInfo.CostDriver, ExclusionCriteria = exclusion ?? new ExclusionCriteria(), ExcludedEncounters = new List(), IncludedEncounters = new List(), Filters = new Biz.Exploration.Filters.Filter { DischargeDateStart = new DateTime(2024, 1, 1), DischargeDateEnd = new DateTime(2025, 1, 1), FilterChipItems = chips } }; } // Gender filter — FPES-level (encounterSummaryFilter placeholder). // GlobalId "Gender" maps to the Gender dimension in the schema service. // The Filters list contains the hierarchy path ID for "Male" as resolved // by the schema service; adjust the Id if needed for your test database. private static FilterChipItem CreateGenderFilter() => new() { ChipType = ChipType.ScoreMember, GlobalId = "Gender", Key = ChipKey.GenderID, Operator = OperatorType.In, Filters = new List { new() { Id = "Male", DisplayText = "Male" } } }; private static FilterChipItem CreateChargesFilter() => new() { ChipType = ChipType.Boolean, Key = ChipKey.IsGreaterThanZeroCharges, IsSelected = true, Operator = OperatorType.In, Filters = new List() }; private ExplorationCostDriverInfo CreateSummaryInfo() => new() { ConfigurationGuid = _configurationGuid, ExclusionCriteria = new ExclusionCriteria(), Filters = new Biz.Exploration.Filters.Filter { DischargeDateStart = new DateTime(2024, 1, 1), DischargeDateEnd = new DateTime(2025, 1, 1), FilterChipItems = new List { new FilterChipItem { ChipType = ChipType.DateRange, Key = ChipKey.DischargeDateID, Filters = [], DateRange = [new DateTime(2024, 1, 1), new DateTime(2025, 1, 1)] } } } }; } }