using FluentAssertions; using NUnit.Framework; using Strata.ContinuousImprovement.Biz.StrategicOpportunities.Dtos; using Strata.ContinuousImprovement.Biz.StrategicOpportunities.Queries; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using VerifyNUnit; namespace Strata.ContinuousImprovement.Biz.Test.Unit.StrategicOpportunities { [ExcludeFromCodeCoverage] [TestFixture, Category("Unit"), Category("CCI")] internal class StrategicOpportunityGLUpdateQueryBuilderTest : StrategicOpportunityTestsBase { private StrategicOpportunityGLUpdateQueryBuilder _queryBuilder; private static readonly IEnumerable Rows = [ StrategicOpportunityDetailDimension.System.Value, StrategicOpportunityDetailDimension.Entity.Value, StrategicOpportunityDetailDimension.Department.Value, StrategicOpportunityDetailDimension.GLAccount.Value, ]; [SetUp] public void Setup() { _queryBuilder = new StrategicOpportunityGLUpdateQueryBuilder(); } [Test] public async Task BuildUpdateQuery_WithFourAdjustments_GeneratesCorrectSql() { // Arrange const long opportunityId = 42; var adjustments = new List { // Root-level adjustment new() { NodeKey = "root", GoalAmountValue = 50000 }, // Entity-level adjustment new() { NodeKey = "101", GoalAmountValue = 20000 }, // Entity + Department level: null (passthrough) new() { NodeKey = "101|201", GoalAmountValue = null }, // Entity + Department + GLAccount (lowest level) new() { NodeKey = "101|201|3001", GoalAmountValue = 15000 }, }; // Act var result = _queryBuilder.BuildUpdateQuery(adjustments, opportunityId, Rows).Query; // Assert await Verifier.Verify(result, VerifySettings); } [Test] public void BuildUpdateQuery_WithAccountTypeOnSecondAdjustment_IncludesAccountTypeFilterInSubquery() { // AccountType filters are embedded in the inner SELECT WHERE for non-first adjustments // (the first adjustment uses an outer WHERE clause that filters by hierarchy only) const long opportunityId = 42; var adjustments = new List { new() { NodeKey = "root", GoalAmountValue = 50000 }, new() { NodeKey = "101|201|3001", GoalAmountValue = 15000, AccountType = 0 }, }; var (query, parameters) = _queryBuilder.BuildUpdateQuery(adjustments, opportunityId, Rows); query.Should().Contain("AccountType = :AccountType_1"); parameters.Should().ContainKey("AccountType_1"); parameters["AccountType_1"].Should().Be((byte)0); } [Test] public void BuildUpdateQuery_WithNullAccountType_OmitsAccountTypeFilter() { const long opportunityId = 42; var adjustments = new List { new() { NodeKey = "root", GoalAmountValue = 50000 }, new() { NodeKey = "101|201|3001", GoalAmountValue = 15000, AccountType = null }, }; var (query, parameters) = _queryBuilder.BuildUpdateQuery(adjustments, opportunityId, Rows); query.Should().NotContain("AccountType = :"); parameters.Should().NotContainKey("AccountType_1"); } [Test] public void BuildUpdateQuery_WithInvalidOpportunityId_ThrowsArgumentException() { var adjustments = new List { new() { NodeKey = "101", GoalAmountValue = 1000 }, }; var act = () => _queryBuilder.BuildUpdateQuery(adjustments, opportunityId: 0, Rows); act.Should().Throw().WithParameterName("opportunityId"); } [Test] public void BuildUpdateQuery_WithEmptyNodeKey_ThrowsArgumentException() { var adjustments = new List { new() { NodeKey = "", GoalAmountValue = 1000 }, }; var act = () => _queryBuilder.BuildUpdateQuery(adjustments, opportunityId: 42, Rows); act.Should().Throw().WithParameterName("nodeKey"); } } }