Files

303 lines
14 KiB
C#

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
{
/// <summary>
/// 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.
/// </summary>
[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<CancellationToken>()))
.ReturnsAsync(new ScoreDimensionInfoDto { DimensionGuid = genderDimensionGuid });
Mock.Get(SchemaServiceClient)
.Setup(s => s.GetDimensionMembersFromHierarchyPathsAsync(genderDimensionGuid, It.IsAny<IEnumerable<string>>(), It.IsAny<CancellationToken>()))
.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<IAsyncDbContextFactory<JazzDbContext>>(),
SchemaServiceClient,
Mock.Of<ILogger<ExplorationFilterService>>());
var featureFlagWrapper = Mock.Of<IFeatureFlagWrapper>();
Mock.Get(featureFlagWrapper)
.Setup(f => f.IsCiBenchmarkingEnabled(It.IsAny<bool>()))
.ReturnsAsync(true);
ExplorationService = new ExplorationService(
JazzConnBuilderFactory,
SnowflakeDatabaseContext,
Mock.Of<IFiscalMonthResolver>(),
featureFlagWrapper,
Mock.Of<IStrataSphereCompareService>(),
filterService,
Mock.Of<ILogger<ExplorationService>>());
// 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<long>(),
IncludedEncounters = new List<long>(),
Filters = new Biz.Exploration.Filters.Filter
{
DischargeDateStart = new DateTime(2024, 1, 1),
DischargeDateEnd = new DateTime(2025, 1, 1),
FilterChipItems = new List<FilterChipItem>
{
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<FilterChipItem> { dateFilter, extraFilter }
: new List<FilterChipItem> { dateFilter };
return new ExplorationDetailInfo
{
ConfigurationGuid = _detailInfo.ConfigurationGuid,
CaseTypeFamilyId = _detailInfo.CaseTypeFamilyId,
CostDriver = _detailInfo.CostDriver,
ExclusionCriteria = exclusion ?? new ExclusionCriteria(),
ExcludedEncounters = new List<long>(),
IncludedEncounters = new List<long>(),
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<TreeNode> { new() { Id = "Male", DisplayText = "Male" } }
};
private static FilterChipItem CreateChargesFilter() => new()
{
ChipType = ChipType.Boolean,
Key = ChipKey.IsGreaterThanZeroCharges,
IsSelected = true,
Operator = OperatorType.In,
Filters = new List<TreeNode>()
};
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<FilterChipItem>
{
new FilterChipItem
{
ChipType = ChipType.DateRange,
Key = ChipKey.DischargeDateID,
Filters = [],
DateRange = [new DateTime(2024, 1, 1), new DateTime(2025, 1, 1)]
}
}
}
};
}
}