using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using FrameworkBLL.MetaReport; using FrameworkBLL.Promotion.Adapters; using FrameworkDAL.CustomCode.Promotion; using FrameworkDAL.DTO.MetaReport; using GB5Shared.DTO.Framework.Login; using Moq; using Xunit; namespace FrameworkTests; /// /// MetaReportPromotionAdapter's real complexity is the two-phase Import: MetaReportBLL.AssignIds /// (read directly, not guessed) assigns real AutoNumber IDs to new Variants/Aspects/Options but /// never remaps Option.ApplicableVariantIds, so a brand-new Option applying to a brand-new Variant /// can't get the right ApplicableVariantIds until a save has actually assigned that Variant its real /// ID. This covers the two-phase sequencing (Phase 1 establishes IDs with empty applicability, Phase /// 2 fills in the now-resolvable ApplicableVariantIds and saves again), the ReportCode/MenuCode /// natural-key resolution per Variant, and the update-merge (matching existing children by name so /// a re-promotion updates in place instead of duplicating). /// /// Mock behavior mimics AssignIds precisely: SaveMetaReportDefinition's callback assigns a real, /// incrementing ID to any Variant/Aspect/Option whose ID is <= 0, and leaves already-real IDs /// untouched — exactly like the real method does. /// public class MetaReportPromotionAdapterTests { private static LoginDTO Login() => new() { ClientId = -1, UserId = 7 }; private static MetaReportDTO OriginDto() => new() { MetaReportId = 10, MetaReportName = "Sales Picker", Description = "Pick sales report variant", Variants = new List { new() { VariantId = 1, VariantName = "Summary", ReportId = 100, MenuId = 0, SortOrder = 1 }, new() { VariantId = 2, VariantName = "Detail", ReportId = 200, MenuId = 300, SortOrder = 2 } }, Aspects = new List { new() { AspectId = 1, AspectName = "Period", AspectType = "radio", DisplayOrder = 1, Options = new List { new() { OptionId = 1, Label = "Monthly", Value = "M", ApplicableVariantIds = new List { 1, 2 } }, new() { OptionId = 2, Label = "Yearly", Value = "Y", ApplicableVariantIds = new List { 2 } } } } } }; private static (Mock bll, Mock reportLookup, Mock menuLookup) Mocks() => (new Mock(), new Mock(), new Mock()); [Fact] public async Task Export_ResolvesCodesAndConvertsApplicabilityToNames() { var (bll, reportLookup, menuLookup) = Mocks(); bll.Setup(b => b.GetMetaReport(10, It.IsAny(), It.IsAny())).ReturnsAsync(OriginDto()); reportLookup.Setup(l => l.GetReportCodeById(100, It.IsAny(), It.IsAny())).ReturnsAsync("SALES_SUMMARY"); reportLookup.Setup(l => l.GetReportCodeById(200, It.IsAny(), It.IsAny())).ReturnsAsync("SALES_DETAIL"); menuLookup.Setup(l => l.GetMenuCodeById(300, It.IsAny(), It.IsAny())).ReturnsAsync("SALES_MENU"); var adapter = new MetaReportPromotionAdapter(bll.Object, reportLookup.Object, menuLookup.Object); var bundle = await adapter.ExportForPromotionAsync(10, Login(), CancellationToken.None); Assert.Equal("METAREPORT", bundle.EntityTypeCode); Assert.Contains("SALES_SUMMARY", bundle.PayloadJson); Assert.Contains("SALES_DETAIL", bundle.PayloadJson); Assert.Contains("SALES_MENU", bundle.PayloadJson); Assert.Contains("Summary", bundle.PayloadJson); // variant name used for applicability, not raw id Assert.Contains("Detail", bundle.PayloadJson); } [Fact] public async Task Export_ReportIdSetButUnresolvable_Throws() { var (bll, reportLookup, menuLookup) = Mocks(); bll.Setup(b => b.GetMetaReport(10, It.IsAny(), It.IsAny())).ReturnsAsync(OriginDto()); reportLookup.Setup(l => l.GetReportCodeById(100, It.IsAny(), It.IsAny())).ReturnsAsync((string?)null); var adapter = new MetaReportPromotionAdapter(bll.Object, reportLookup.Object, menuLookup.Object); await Assert.ThrowsAsync( () => adapter.ExportForPromotionAsync(10, Login(), CancellationToken.None)); } private static void SetupAssignIdsMimic(Mock bll, int nextVariantId, int nextAspectId, int nextOptionId) { int v = nextVariantId, a = nextAspectId, o = nextOptionId; bll.Setup(b => b.SaveMetaReportDefinition(It.IsAny(), It.IsAny(), It.IsAny())) .Callback((dto, _, _) => { if (dto.MetaReportId <= 0) dto.MetaReportId = 900; foreach (var variant in dto.Variants) if (variant.VariantId <= 0) variant.VariantId = v++; foreach (var aspect in dto.Aspects) { if (aspect.AspectId <= 0) aspect.AspectId = a++; foreach (var option in aspect.Options) if (option.OptionId <= 0) option.OptionId = o++; } }) .ReturnsAsync("Saved"); } private static async Task<(MetaReportPromotionAdapter adapter, Mock bll)> AdapterForNewImport() { var (bll, reportLookup, menuLookup) = Mocks(); reportLookup.Setup(l => l.GetReportIdByCode("SALES_SUMMARY", It.IsAny(), It.IsAny())).ReturnsAsync(1100); reportLookup.Setup(l => l.GetReportIdByCode("SALES_DETAIL", It.IsAny(), It.IsAny())).ReturnsAsync(1200); menuLookup.Setup(l => l.GetMenuIdByCode("SALES_MENU", It.IsAny(), It.IsAny())).ReturnsAsync(1300); SetupAssignIdsMimic(bll, nextVariantId: 501, nextAspectId: 601, nextOptionId: 701); return (new MetaReportPromotionAdapter(bll.Object, reportLookup.Object, menuLookup.Object), bll); } private static async Task ExportedBundle() { var (bll, reportLookup, menuLookup) = Mocks(); bll.Setup(b => b.GetMetaReport(10, It.IsAny(), It.IsAny())).ReturnsAsync(OriginDto()); reportLookup.Setup(l => l.GetReportCodeById(100, It.IsAny(), It.IsAny())).ReturnsAsync("SALES_SUMMARY"); reportLookup.Setup(l => l.GetReportCodeById(200, It.IsAny(), It.IsAny())).ReturnsAsync("SALES_DETAIL"); menuLookup.Setup(l => l.GetMenuCodeById(300, It.IsAny(), It.IsAny())).ReturnsAsync("SALES_MENU"); var exportAdapter = new MetaReportPromotionAdapter(bll.Object, reportLookup.Object, menuLookup.Object); return await exportAdapter.ExportForPromotionAsync(10, Login(), CancellationToken.None); } [Fact] public async Task Import_NewMetaReport_TwoPhaseSaveResolvesApplicability() { var bundle = await ExportedBundle(); var (adapter, bll) = await AdapterForNewImport(); var newId = await adapter.ImportPromotedAsync(bundle, destinationSourceType: 0, existingDestinationId: null, Login(), CancellationToken.None); Assert.Equal(900, newId); // Saved exactly twice — Phase 1 (establish IDs) and Phase 2 (fill in applicability). bll.Verify(b => b.SaveMetaReportDefinition(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); // The FINAL saved state (2nd call's argument) has correct, resolved ApplicableVariantIds — // capture it via a fresh callback-free re-verify using the invocation history. var finalCallArg = bll.Invocations .Where(i => i.Method.Name == nameof(IMetaReportBLL.SaveMetaReportDefinition)) .Select(i => (MetaReportDTO)i.Arguments[0]) .Last(); var summaryVariantId = finalCallArg.Variants.Single(v => v.VariantName == "Summary").VariantId; var detailVariantId = finalCallArg.Variants.Single(v => v.VariantName == "Detail").VariantId; var monthlyOption = finalCallArg.Aspects.Single().Options.Single(o => o.Label == "Monthly"); var yearlyOption = finalCallArg.Aspects.Single().Options.Single(o => o.Label == "Yearly"); Assert.Equal(new[] { summaryVariantId, detailVariantId }.OrderBy(x => x), monthlyOption.ApplicableVariantIds.OrderBy(x => x)); Assert.Equal(new[] { detailVariantId }, yearlyOption.ApplicableVariantIds); // ReportId/MenuId resolved to the DESTINATION's own local ids, not the origin's. Assert.Equal(1100, finalCallArg.Variants.Single(v => v.VariantName == "Summary").ReportId); Assert.Equal(1200, finalCallArg.Variants.Single(v => v.VariantName == "Detail").ReportId); Assert.Equal(1300, finalCallArg.Variants.Single(v => v.VariantName == "Detail").MenuId); } [Fact] public async Task Import_UnknownReportCode_ThrowsBeforeSaving() { var bundle = await ExportedBundle(); var (bll, reportLookup, menuLookup) = Mocks(); reportLookup.Setup(l => l.GetReportIdByCode("SALES_SUMMARY", It.IsAny(), It.IsAny())).ReturnsAsync((int?)null); var adapter = new MetaReportPromotionAdapter(bll.Object, reportLookup.Object, menuLookup.Object); await Assert.ThrowsAsync( () => adapter.ImportPromotedAsync(bundle, 0, null, Login(), CancellationToken.None)); bll.Verify(b => b.SaveMetaReportDefinition( It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Fact] public async Task Import_ExistingDestination_MergesChildrenByNameRatherThanDuplicating() { var bundle = await ExportedBundle(); var (bll, reportLookup, menuLookup) = Mocks(); reportLookup.Setup(l => l.GetReportIdByCode("SALES_SUMMARY", It.IsAny(), It.IsAny())).ReturnsAsync(1100); reportLookup.Setup(l => l.GetReportIdByCode("SALES_DETAIL", It.IsAny(), It.IsAny())).ReturnsAsync(1200); menuLookup.Setup(l => l.GetMenuIdByCode("SALES_MENU", It.IsAny(), It.IsAny())).ReturnsAsync(1300); // An existing destination MetaReport already has these exact variants/aspects/options, // under DIFFERENT (destination-local) IDs than the origin's. var existingDestination = new MetaReportDTO { MetaReportId = 42, MetaReportName = "Sales Picker", Variants = new List { new() { VariantId = 501, VariantName = "Summary", ReportId = 1100 }, new() { VariantId = 502, VariantName = "Detail", ReportId = 1200, MenuId = 1300 } }, Aspects = new List { new() { AspectId = 601, AspectName = "Period", Options = new List { new() { OptionId = 701, Label = "Monthly", ApplicableVariantIds = new List { 501, 502 } }, new() { OptionId = 702, Label = "Yearly", ApplicableVariantIds = new List { 502 } } } } } }; bll.Setup(b => b.GetMetaReport(42, It.IsAny(), It.IsAny())).ReturnsAsync(existingDestination); // AssignIds-mimic: any id already >0 stays untouched (matches real AssignIds behavior). bll.Setup(b => b.SaveMetaReportDefinition(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync("Saved"); var adapter = new MetaReportPromotionAdapter(bll.Object, reportLookup.Object, menuLookup.Object); var resultId = await adapter.ImportPromotedAsync(bundle, 0, existingDestinationId: 42, Login(), CancellationToken.None); Assert.Equal(42, resultId); var firstCallArg = (MetaReportDTO)bll.Invocations .First(i => i.Method.Name == nameof(IMetaReportBLL.SaveMetaReportDefinition)).Arguments[0]; // Existing IDs reused — never re-created as new (which would have been <= 0 going in). Assert.Equal(501, firstCallArg.Variants.Single(v => v.VariantName == "Summary").VariantId); Assert.Equal(502, firstCallArg.Variants.Single(v => v.VariantName == "Detail").VariantId); Assert.Equal(601, firstCallArg.Aspects.Single().AspectId); Assert.Equal(701, firstCallArg.Aspects.Single().Options.Single(o => o.Label == "Monthly").OptionId); Assert.Equal(702, firstCallArg.Aspects.Single().Options.Single(o => o.Label == "Yearly").OptionId); } }