using System.Threading;
using System.Threading.Tasks;
using FrameworkBLL.Promotion.Adapters;
using FrameworkBLL.ReportViewer;
using FrameworkDAL.CustomCode.Promotion;
using FrameworkDAL.DTO.ReportViewer;
using GB5Shared.DTO.Framework.Login;
using GB5Shared.DTO.Promotion;
using Moq;
using Newtonsoft.Json;
using Xunit;
namespace FrameworkTests;
///
/// ReportViewPromotionAdapter's real job is resolving cross-environment references via their
/// natural keys (ReportId -> ReportCode, and — now that ReportHeaderPromotionAdapter exists —
/// ReportHeaderId -> ReportHeaderCode too) and conservatively blocking the ones with no
/// remapping support yet (Template/Page/Menu/ClientIds). Also covers that the "risky" raw fields
/// genuinely reach ValidateForPromotionAsync (they're deliberately still present in the bundle
/// payload precisely so this validator can see them, even though ImportPromotedAsync never maps
/// them onto the destination row).
///
public class ReportViewPromotionAdapterTests
{
private static LoginDTO Login() => new() { ClientId = -1, UserId = 7 };
private static ReportViewDTO CleanDto(int reportViewId = 42, int reportId = 100) => new()
{
ReportViewId = reportViewId,
ReportId = reportId,
ReportViewName = "Sales Summary",
ReportViewType = 0,
ClientIds = ",-1",
TemplateId = -1,
SecondTemplateId = -1,
PageId = -1,
ReportHeaderId = -1,
ApplicableMenuIds = null,
OrderByFields = string.Empty,
DynamicFooter = string.Empty,
ReportTitle = "NONE",
ReportViewPivotFormat = string.Empty
};
private static (Mock bll, Mock reportLookup, Mock headerLookup) Mocks()
=> (new Mock(), new Mock(), new Mock());
[Fact]
public async Task Export_ResolvesReportCode_AndBundlesPortableFields()
{
var (bll, reportLookup, headerLookup) = Mocks();
bll.Setup(b => b.GetReportView(42, It.IsAny(), It.IsAny()))
.ReturnsAsync(JsonConvert.SerializeObject(CleanDto()));
reportLookup.Setup(l => l.GetReportCodeById(100, It.IsAny(), It.IsAny()))
.ReturnsAsync("SALES_RPT");
var adapter = new ReportViewPromotionAdapter(bll.Object, reportLookup.Object, headerLookup.Object);
var bundle = await adapter.ExportForPromotionAsync(42, Login(), CancellationToken.None);
Assert.Equal("REPORTVIEW", bundle.EntityTypeCode);
Assert.Contains("SALES_RPT", bundle.PayloadJson);
Assert.Contains("Sales Summary", bundle.PayloadJson);
}
[Fact]
public async Task Export_ReportHasNoCode_Throws()
{
var (bll, reportLookup, headerLookup) = Mocks();
bll.Setup(b => b.GetReportView(42, It.IsAny(), It.IsAny()))
.ReturnsAsync(JsonConvert.SerializeObject(CleanDto()));
reportLookup.Setup(l => l.GetReportCodeById(100, It.IsAny(), It.IsAny()))
.ReturnsAsync((string?)null);
var adapter = new ReportViewPromotionAdapter(bll.Object, reportLookup.Object, headerLookup.Object);
await Assert.ThrowsAsync(
() => adapter.ExportForPromotionAsync(42, Login(), CancellationToken.None));
}
[Fact]
public async Task Export_ReportHeaderSetButNoCode_Throws()
{
var dto = CleanDto();
dto.ReportHeaderId = 200;
var (bll, reportLookup, headerLookup) = Mocks();
bll.Setup(b => b.GetReportView(dto.ReportViewId, It.IsAny(), It.IsAny()))
.ReturnsAsync(JsonConvert.SerializeObject(dto));
reportLookup.Setup(l => l.GetReportCodeById(dto.ReportId, It.IsAny(), It.IsAny()))
.ReturnsAsync("SALES_RPT");
headerLookup.Setup(l => l.GetReportHeaderCodeById(200, It.IsAny(), It.IsAny()))
.ReturnsAsync((string?)null);
var adapter = new ReportViewPromotionAdapter(bll.Object, reportLookup.Object, headerLookup.Object);
await Assert.ThrowsAsync(
() => adapter.ExportForPromotionAsync(dto.ReportViewId, Login(), CancellationToken.None));
}
private static async Task BuildBundle(
ReportViewDTO dto, string reportCode = "SALES_RPT", string? reportHeaderCode = null)
{
var (bll, reportLookup, headerLookup) = Mocks();
bll.Setup(b => b.GetReportView(dto.ReportViewId, It.IsAny(), It.IsAny()))
.ReturnsAsync(JsonConvert.SerializeObject(dto));
reportLookup.Setup(l => l.GetReportCodeById(dto.ReportId, It.IsAny(), It.IsAny()))
.ReturnsAsync(reportCode);
if (dto.ReportHeaderId != 0 && dto.ReportHeaderId != -1)
headerLookup.Setup(l => l.GetReportHeaderCodeById(dto.ReportHeaderId, It.IsAny(), It.IsAny()))
.ReturnsAsync(reportHeaderCode);
var adapter = new ReportViewPromotionAdapter(bll.Object, reportLookup.Object, headerLookup.Object);
return await adapter.ExportForPromotionAsync(dto.ReportViewId, Login(), CancellationToken.None);
}
[Fact]
public async Task Validate_CleanReportView_IsOk()
{
var bundle = await BuildBundle(CleanDto());
var (bll, reportLookup, headerLookup) = Mocks();
var adapter = new ReportViewPromotionAdapter(bll.Object, reportLookup.Object, headerLookup.Object);
var result = await adapter.ValidateForPromotionAsync(bundle, CancellationToken.None);
Assert.True(result.IsPromotable);
}
[Fact]
public async Task Validate_ReportHeaderSet_NoLongerBlocks()
{
var dto = CleanDto();
dto.ReportHeaderId = 200;
var bundle = await BuildBundle(dto, reportHeaderCode: "STD_HEADER");
var (bll, reportLookup, headerLookup) = Mocks();
var adapter = new ReportViewPromotionAdapter(bll.Object, reportLookup.Object, headerLookup.Object);
var result = await adapter.ValidateForPromotionAsync(bundle, CancellationToken.None);
Assert.True(result.IsPromotable);
}
[Theory]
[InlineData("clientids")]
[InlineData("templateid")]
[InlineData("secondtemplateid")]
[InlineData("pageid")]
[InlineData("applicablemenuids")]
public async Task Validate_RiskyFieldSet_Blocks(string whichField)
{
var dto = CleanDto();
switch (whichField)
{
case "clientids": dto.ClientIds = ",7,9"; break;
case "templateid": dto.TemplateId = 55; break;
case "secondtemplateid": dto.SecondTemplateId = 56; break;
case "pageid": dto.PageId = 57; break;
case "applicablemenuids": dto.ApplicableMenuIds = ",1,2,"; break;
}
var bundle = await BuildBundle(dto);
var (bll, reportLookup, headerLookup) = Mocks();
var adapter = new ReportViewPromotionAdapter(bll.Object, reportLookup.Object, headerLookup.Object);
var result = await adapter.ValidateForPromotionAsync(bundle, CancellationToken.None);
Assert.False(result.IsPromotable);
Assert.NotEmpty(result.BlockingReasons);
}
[Fact]
public async Task Import_NewRow_ResolvesReportCodeAndSaves()
{
var bundle = await BuildBundle(CleanDto(), "SALES_RPT");
var (bll, reportLookup, headerLookup) = Mocks();
reportLookup.Setup(l => l.GetReportIdByCode("SALES_RPT", It.IsAny(), It.IsAny()))
.ReturnsAsync(777);
// Capture the state AS PASSED IN (before this same callback mutates it to simulate
// SaveReportView assigning a new ID) — Moq's Verify re-inspects the live object later, so
// asserting ReportViewId==0 there would actually observe the post-mutation value instead.
int? reportIdAsPassedIn = null;
byte? sourceTypeAsPassedIn = null;
int? reportViewIdAsPassedIn = null;
bll.Setup(b => b.SaveReportView(It.IsAny(), It.IsAny(), It.IsAny()))
.Callback((d, _, _) =>
{
reportIdAsPassedIn = d.ReportId;
sourceTypeAsPassedIn = d.ReportViewSourceType;
reportViewIdAsPassedIn = d.ReportViewId;
d.ReportViewId = 901; // simulates SaveReportView assigning the new AutoNumber id
})
.ReturnsAsync("Saved 901");
var adapter = new ReportViewPromotionAdapter(bll.Object, reportLookup.Object, headerLookup.Object);
var newId = await adapter.ImportPromotedAsync(bundle, destinationSourceType: 3, existingDestinationId: null, Login(), CancellationToken.None);
Assert.Equal(901, newId);
Assert.Equal(777, reportIdAsPassedIn);
Assert.Equal((byte)3, sourceTypeAsPassedIn);
Assert.Equal(0, reportViewIdAsPassedIn);
bll.Verify(b => b.SaveReportView(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once);
}
// Regression for a real bug found live 2026-08-21 (first-ever real Promotion Import execution):
// MREPORTVIEW.TEMPLATEID/PAGEID are NOT NULL with FKs to MFILE, which has a real dummy row at
// FILEID=-1 but none at FILEID=0 — leaving the destination DTO at its own bare-0 default (as
// it was before this fix) threw a genuine FK violation on every single import.
[Fact]
public async Task Import_StampsTemplateIdAndPageIdToTheirRealSentinel_NotTheDtoDefaultZero()
{
var dto = CleanDto();
var bundle = await BuildBundle(dto, "SALES_RPT", null);
var (bll, reportLookup, headerLookup) = Mocks();
reportLookup.Setup(l => l.GetReportIdByCode("SALES_RPT", It.IsAny(), It.IsAny()))
.ReturnsAsync(777);
int? capturedTemplateId = null, capturedPageId = null;
bll.Setup(b => b.SaveReportView(It.IsAny(), It.IsAny(), It.IsAny()))
.Callback((d, _, _) =>
{
capturedTemplateId = d.TemplateId;
capturedPageId = d.PageId;
})
.ReturnsAsync("Saved");
var adapter = new ReportViewPromotionAdapter(bll.Object, reportLookup.Object, headerLookup.Object);
await adapter.ImportPromotedAsync(bundle, 3, null, Login(), CancellationToken.None);
Assert.Equal(-1, capturedTemplateId);
Assert.Equal(-1, capturedPageId);
}
[Fact]
public async Task Import_ReportHeaderCodeSet_ResolvesToLocalReportHeaderId()
{
var dto = CleanDto();
dto.ReportHeaderId = 200;
var bundle = await BuildBundle(dto, "SALES_RPT", "STD_HEADER");
var (bll, reportLookup, headerLookup) = Mocks();
reportLookup.Setup(l => l.GetReportIdByCode("SALES_RPT", It.IsAny(), It.IsAny()))
.ReturnsAsync(777);
headerLookup.Setup(l => l.GetReportHeaderIdByCode("STD_HEADER", It.IsAny(), It.IsAny()))
.ReturnsAsync(555);
int? capturedReportHeaderId = null;
bll.Setup(b => b.SaveReportView(It.IsAny(), It.IsAny(), It.IsAny()))
.Callback((d, _, _) => capturedReportHeaderId = d.ReportHeaderId)
.ReturnsAsync("Saved");
var adapter = new ReportViewPromotionAdapter(bll.Object, reportLookup.Object, headerLookup.Object);
await adapter.ImportPromotedAsync(bundle, 3, null, Login(), CancellationToken.None);
Assert.Equal(555, capturedReportHeaderId);
}
[Fact]
public async Task Import_NoReportHeaderCode_LeavesReportHeaderIdAtSentinel()
{
var bundle = await BuildBundle(CleanDto()); // ReportHeaderId == -1, no code in payload
var (bll, reportLookup, headerLookup) = Mocks();
reportLookup.Setup(l => l.GetReportIdByCode("SALES_RPT", It.IsAny(), It.IsAny()))
.ReturnsAsync(777);
int? capturedReportHeaderId = null;
bll.Setup(b => b.SaveReportView(It.IsAny(), It.IsAny(), It.IsAny()))
.Callback((d, _, _) => capturedReportHeaderId = d.ReportHeaderId)
.ReturnsAsync("Saved");
var adapter = new ReportViewPromotionAdapter(bll.Object, reportLookup.Object, headerLookup.Object);
await adapter.ImportPromotedAsync(bundle, 3, null, Login(), CancellationToken.None);
Assert.Equal(-1, capturedReportHeaderId);
headerLookup.Verify(l => l.GetReportHeaderIdByCode(
It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
}
[Fact]
public async Task Import_UnknownReportCode_ThrowsBeforeSaving()
{
var bundle = await BuildBundle(CleanDto(), "MISSING_RPT");
var (bll, reportLookup, headerLookup) = Mocks();
reportLookup.Setup(l => l.GetReportIdByCode("MISSING_RPT", It.IsAny(), It.IsAny()))
.ReturnsAsync((int?)null);
var adapter = new ReportViewPromotionAdapter(bll.Object, reportLookup.Object, headerLookup.Object);
await Assert.ThrowsAsync(
() => adapter.ImportPromotedAsync(bundle, 3, null, Login(), CancellationToken.None));
bll.Verify(b => b.SaveReportView(
It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
}
[Fact]
public async Task Import_ExistingDestinationId_UpdatesRatherThanInsertsNew()
{
var bundle = await BuildBundle(CleanDto(), "SALES_RPT");
var (bll, reportLookup, headerLookup) = Mocks();
reportLookup.Setup(l => l.GetReportIdByCode("SALES_RPT", It.IsAny(), It.IsAny()))
.ReturnsAsync(777);
bll.Setup(b => b.SaveReportView(It.IsAny(), It.IsAny(), It.IsAny()))
.ReturnsAsync("Updated");
var adapter = new ReportViewPromotionAdapter(bll.Object, reportLookup.Object, headerLookup.Object);
var resultId = await adapter.ImportPromotedAsync(bundle, 3, existingDestinationId: 501, Login(), CancellationToken.None);
Assert.Equal(501, resultId);
bll.Verify(b => b.SaveReportView(
It.Is(d => d.ReportViewId == 501),
It.IsAny(), It.IsAny()), Times.Once);
}
}