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);
}
}