using CsvHelper; using CsvHelper.Configuration; using Microsoft.Extensions.DependencyInjection; using Strata.ContinuousImprovement.Biz.Shared; using Strata.ContinuousImprovement.Biz.StrategicOpportunities; using Strata.ContinuousImprovement.Biz.StrategicOpportunities.DistributionProcess; using Strata.ContinuousImprovement.Biz.StrategicOpportunities.DistributionProcess.ChargeCode; using Strata.ContinuousImprovement.Biz.StrategicOpportunities.Enums; using Strata.ContinuousImprovement.Biz.Test.Unit.Utilities; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Strata.ContinuousImprovement.Biz.Test.Integration.SnowflakeIntegrationTests.DistributionProcess { [ExcludeFromCodeCoverage] [TestFixture, Category("Integration"), Category("CCI")] public class DistributionProcessChargeCodeTests : IntegrationTestBase { private const string SourceFilePath = @"SnowflakeIntegrationTests/DistributionProcess/TestData/ChargeCode"; private static readonly ImmutableDictionary Source, Period Target)> _dataPeriod = new Dictionary Source, Period Target)>() { { "AllMonthsAreCross", (new Period(new DateOnly(2022, 12, 1), new DateOnly(2023, 12, 31)), new Period(new DateOnly(2024, 1, 1), new DateOnly(2024, 12, 31))) }, { "AllMonthsAreNotCross", (new Period(new DateOnly(2023, 1, 1), new DateOnly(2023, 5, 30)), new Period(new DateOnly(2023, 6, 1), new DateOnly(2023, 12, 31))) }, { "MixedMonths", (new Period(new DateOnly(2022, 12, 1), new DateOnly(2023, 8, 31)), new Period(new DateOnly(2023, 5, 1), new DateOnly(2023, 12, 31))) } }.ToImmutableDictionary(); private IDistributionProcessFactory _distributionProcessFactory; private const long TestOpportunityId = 1; private Period _baselinePeriod; private Period _goalPeriod; private Period _trackingPeriod; private double[] _rampUps; [OneTimeSetUp] public async Task Setup() { await CleanData(); await SeedDataFromCsv("BaselineData.csv"); _distributionProcessFactory = ServiceProvider.GetRequiredService(); _baselinePeriod = new Period(new DateOnly(2023, 1, 1), new DateOnly(2023, 12, 31)); _goalPeriod = new Period(new DateOnly(2024, 1, 1), new DateOnly(2024, 12, 31)); _trackingPeriod = new Period(new DateOnly(2024, 1, 1), new DateOnly(2024, 12, 31)); _rampUps = new double[] { 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0 }; } [OneTimeTearDown] public async Task Cleanup() { await CleanData(); } [TearDown] public async Task CleanupTestData() { await CleanData(false); } [TestCaseSource(nameof(DistributeAsync_FromSourceToOpportunity_TestCases))] public async Task DistributeAsync_FromSourceToOpportunity ( DistributionMethod distributionMethod, Period sourcePeriod, Period targetPeriod, double[] rampUps ) { // Arrange var distributionProcess = _distributionProcessFactory.CreateDistributionProcessChargeCode( TestOpportunityId, sourcePeriod ?? _baselinePeriod, targetPeriod ?? _goalPeriod, _trackingPeriod, []); // Act var summary = await distributionProcess.DistributeAsync(distributionMethod, DistributionStage.FromSourceToOpportunity); // Assert var sbResult = new StringBuilder(); var details = await GetDetailRecords(isBaseline: false); sbResult.AppendLine(ConvertListDetailToCsvString(details)); sbResult.AppendLine(ConvertSummaryToCsvString(summary)); await Verifier.Verify(sbResult.ToString(), verifySettings) .UseFileName(TestContext.CurrentContext.TestName()); } [TestCaseSource(nameof(DistributeAsync_FromOpportunityToInitiative_TestCases))] public async Task DistributeAsync_FromOpportunityToInitiative ( DistributionMethod distributionMethod, Period sourcePeriod, Period targetPeriod, double[] rampUps ) { // Arrange await SeedDataFromCsv("GoalData.csv"); var distributionProcess = _distributionProcessFactory.CreateDistributionProcessChargeCode( TestOpportunityId, _baselinePeriod, sourcePeriod ?? _goalPeriod, targetPeriod ?? _trackingPeriod, rampUps ?? _rampUps); // Act var summary = await distributionProcess.DistributeAsync(distributionMethod, DistributionStage.FromOpportunityToInitiative); // Assert var sbResult = new StringBuilder(); var details = await GetDetailRecords(isBaseline: false); sbResult.AppendLine(ConvertListDetailToCsvString(details)); sbResult.AppendLine(ConvertSummaryToCsvString(summary)); await Verifier.Verify(sbResult.ToString(), verifySettings) .UseFileName(TestContext.CurrentContext.TestName()); } [TestCaseSource(nameof(DistributeAsync_FromInitiativeToOpportunity_TestCases))] public async Task DistributeAsync_FromInitiativeToOpportunity ( DistributionMethod distributionMethod, Period sourcePeriod, Period targetPeriod, double[] rampUps ) { // Arrange await SeedDataFromCsv("GoalData.csv"); var distributionProcess = _distributionProcessFactory.CreateDistributionProcessChargeCode( TestOpportunityId, _baselinePeriod, sourcePeriod ?? _goalPeriod, targetPeriod ?? _trackingPeriod, rampUps ?? _rampUps); // Act var summary = await distributionProcess.DistributeAsync(distributionMethod, DistributionStage.FromInitiativeToOpportunity); // Assert var sbResult = new StringBuilder(); var details = await GetDetailRecords(isBaseline: false); sbResult.AppendLine(ConvertListDetailToCsvString(details)); sbResult.AppendLine(ConvertSummaryToCsvString(summary)); await Verifier.Verify(sbResult.ToString(), verifySettings) .UseFileName(TestContext.CurrentContext.TestName()); } [TestCaseSource(nameof(UpdateDetailCommittedByRampUp_TestCases))] public async Task UpdateDetailCommittedByRampUp ( double[] oldRampUps, double[] newRampUps ) { // Arrange await SeedDataFromCsv("GoalData.csv"); var distributionProcess = _distributionProcessFactory.CreateDistributionProcessChargeCode( TestOpportunityId, _baselinePeriod, _goalPeriod, _trackingPeriod, newRampUps ?? _rampUps); // Act var summary = await distributionProcess.UpdateDetailCommittedByRampUp(oldRampUps); // Assert var sbResult = new StringBuilder(); var details = await GetDetailRecords(isBaseline: false); sbResult.AppendLine(ConvertListDetailToCsvString(details)); sbResult.AppendLine(ConvertSummaryToCsvString(summary)); await Verifier.Verify(sbResult.ToString(), verifySettings) .UseFileName(TestContext.CurrentContext.TestName()); } [TestCaseSource(nameof(GetDistributionSummary_TestCases))] public async Task GetDistributionSummary ( bool isInitiative, double[] rampUps ) { // Arrange await SeedDataFromCsv("GoalData.csv"); var distributionProcess = _distributionProcessFactory.CreateDistributionProcessChargeCode( TestOpportunityId, _baselinePeriod, _goalPeriod, _trackingPeriod, rampUps ?? _rampUps); // Act var summary = await distributionProcess.GetDistributionSummaryAsync(isInitiative); // Assert var sbResult = new StringBuilder(); sbResult.AppendLine(ConvertSummaryToCsvString(summary)); await Verifier.Verify(sbResult.ToString(), verifySettings) .UseFileName(TestContext.CurrentContext.TestName()); } #region Test Case Sources private static IEnumerable DistributeAsync_FromSourceToOpportunity_TestCases() { var distributionStageName = DistributionStage.FromSourceToOpportunity.ToString(); return DistributeAsync_TestCases(distributionStageName); } private static IEnumerable DistributeAsync_FromOpportunityToInitiative_TestCases() { var distributionStageName = DistributionStage.FromOpportunityToInitiative.ToString(); foreach (var x in DistributeAsync_TestCases(distributionStageName)) yield return x; if (_dataPeriod.TryGetValue("AllMonthsAreCross", out var periods)) { yield return new TestCaseData( DistributionMethod.Average, periods.Source, periods.Target, Array.Empty() ).SetName($"DistributeAsync_{distributionStageName}_Average_AllMonthsAreCross_WithoutRampups"); } } private static IEnumerable DistributeAsync_FromInitiativeToOpportunity_TestCases() { var distributionStageName = DistributionStage.FromInitiativeToOpportunity.ToString(); foreach (var x in DistributeAsync_TestCases(distributionStageName)) yield return x; if (_dataPeriod.TryGetValue("AllMonthsAreCross", out var periods)) { yield return new TestCaseData( DistributionMethod.Average, periods.Source, periods.Target, Array.Empty() ).SetName($"DistributeAsync_{distributionStageName}_Average_AllMonthsAreCross_WithoutRampups"); } } private static IEnumerable DistributeAsync_TestCases(string distributionStageName) { if (_dataPeriod.TryGetValue("AllMonthsAreCross", out var periods)) { yield return new TestCaseData( DistributionMethod.Average, periods.Source, periods.Target, null ).SetName($"DistributeAsync_{distributionStageName}_Average_AllMonthsAreCross"); } foreach (var x in _dataPeriod) { yield return new TestCaseData( DistributionMethod.Monthly, x.Value.Source, x.Value.Target, null ).SetName($"DistributeAsync_{distributionStageName}_Monthly_{x.Key}"); } } private static IEnumerable UpdateDetailCommittedByRampUp_TestCases() { var rampUp = new double[] { 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0 }; yield return new TestCaseData( rampUp, rampUp.Reverse().ToArray() ).SetName($"UpdateDetailCommittedByRampUp_RampUpToRampUp"); yield return new TestCaseData( rampUp, Array.Empty() ).SetName($"UpdateDetailCommittedByRampUp_RampUpToNoRampUp"); yield return new TestCaseData( Array.Empty(), rampUp ).SetName($"UpdateDetailCommittedByRampUp_NoRampUpToRampUp"); yield return new TestCaseData( Array.Empty(), Array.Empty() ).SetName($"UpdateDetailCommittedByRampUp_NoRampUpToNoRampUp"); yield return new TestCaseData( rampUp, rampUp ).SetName($"UpdateDetailCommittedByRampUp_NoChangeRampUp"); } private static IEnumerable GetDistributionSummary_TestCases() { yield return new TestCaseData( true, null ).SetName($"GetDistributionSummary_Initiative"); yield return new TestCaseData( true, Array.Empty() ).SetName($"GetDistributionSummary_Initiative_NoRampUp"); yield return new TestCaseData( false, null ).SetName($"GetDistributionSummary_Opportunity"); yield return new TestCaseData( false, Array.Empty() ).SetName($"GetDistributionSummary_Opportunity_NoRampUp"); } #endregion #region Helper Methods private async Task SeedDataFromCsv(string fileName) { var filePath = Path.IsPathRooted(fileName) ? fileName : Path.Combine(AppContext.BaseDirectory, SourceFilePath, fileName); var insertSql = await GetSqlInsertFromCsv(filePath, StrategicOpportunityService.StrategicOpportunityDetailTableName); await SnowflakeDatabaseContext.ExecuteCommandAsync(insertSql); } private async Task GetSqlInsertFromCsv(string filePath, string tableName) { if (!File.Exists(filePath)) { throw new FileNotFoundException($"Test data file not found: {filePath}"); } var lines = await File.ReadAllLinesAsync(filePath); if (lines.Length < 2) { throw new InvalidOperationException($"CSV file {filePath} must contain a header and at least one data row"); } var values = lines.Skip(1).Select(x => $"({x})").ToList(); var insertSql = $@" INSERT INTO {tableName} ({lines[0]}) VALUES {string.Join(",\n", values)} "; return insertSql; } private string ConvertListDetailToCsvString(IEnumerable records) { using (var writer = new StringWriter()) using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) { csv.Context.RegisterClassMap(); csv.WriteRecords(records); return writer.ToString(); } } private string ConvertSummaryToCsvString(DistributionChargeCodeSummary summary) { using (var writer = new StringWriter()) using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) { csv.WriteRecords([summary]); return writer.ToString(); } } private async Task> GetDetailRecords(bool? isBaseline = null) { var sql = $@" SELECT * FROM {StrategicOpportunityService.StrategicOpportunityDetailTableName} WHERE OpportunityId = {TestOpportunityId} {(isBaseline.HasValue ? $"AND IsBaseline = {isBaseline}" : "")} ORDER BY DischargeFiscalYearId, DischargeFiscalMonthId, EntityId "; var records = await SnowflakeDatabaseContext.QueryAsync(sql, nameof(GetDetailRecords)); return records; } private async Task CleanData(bool? isBaseline = null) { await SnowflakeDatabaseContext.ExecuteCommandAsync($@" DELETE FROM {StrategicOpportunityService.StrategicOpportunityDetailTableName} WHERE OpportunityId = {TestOpportunityId} {(isBaseline.HasValue ? $"AND IsBaseline = {isBaseline}" : "")} "); } #endregion } public class StrategicOpportunityDetailCsvMap : ClassMap { public StrategicOpportunityDetailCsvMap() { AutoMap(CultureInfo.InvariantCulture); Map(m => m.RowId).Ignore(); } } }