using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using FrameworkBLL.Promotion;
using FrameworkDAL.CustomCode.Promotion;
using GB5Shared.DTO.Framework.Login;
using GB5Shared.DTO.Promotion;
using GB5Shared.Promotion;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
namespace FrameworkTests;
///
/// PromotionBLL is the single implementation behind both the synchronous "Export Package" HTTP
/// endpoint and PromotionExportNodeExecutor (the GOP node kept for a possible future automated
/// trigger) — this covers BuildExportBundleAsync's validation-blocking behavior directly, plus
/// BuildPackageAsync/OpenPackageAsync's round trip through a mocked (not real-crypto)
/// IPromotionPackageCrypto, since the crypto itself is covered separately in
/// PromotionPackageCryptoTests.
///
public class PromotionBLLTests
{
private static LoginDTO Login() => new() { ClientId = -1, UserId = 7 };
private static IServiceProvider ServiceProviderWith(string entityTypeCode, IPromotableEntity entity)
{
var services = new ServiceCollection();
services.AddKeyedSingleton(entityTypeCode, entity);
return services.BuildServiceProvider();
}
[Fact]
public async Task BuildExportBundleAsync_UnknownEntityType_ThrowsBeforeExporting()
{
var sp = new ServiceCollection().BuildServiceProvider(); // nothing registered for "MENU"
var bll = new PromotionBLL(sp, Mock.Of(), Mock.Of(), NullLogger.Instance);
await Assert.ThrowsAsync(
() => bll.BuildExportBundleAsync("MENU", 42, Login(), CancellationToken.None));
}
[Fact]
public async Task BuildExportBundleAsync_ValidationBlocked_ThrowsWithReason()
{
var entity = new Mock();
entity.Setup(e => e.ExportForPromotionAsync(42, It.IsAny(), It.IsAny()))
.ReturnsAsync(new PromotionBundleDTO { EntityTypeCode = "MENU", OriginEntityId = 42 });
entity.Setup(e => e.ValidateForPromotionAsync(It.IsAny(), It.IsAny()))
.ReturnsAsync(PromotionValidationResultDTO.Block("references a client-only addon column"));
var sp = ServiceProviderWith("MENU", entity.Object);
var bll = new PromotionBLL(sp, Mock.Of(), Mock.Of(), NullLogger.Instance);
var ex = await Assert.ThrowsAsync(
() => bll.BuildExportBundleAsync("MENU", 42, Login(), CancellationToken.None));
Assert.Contains("addon", ex.Message);
}
[Fact]
public async Task BuildExportBundleAsync_Promotable_ReturnsTheBundle()
{
var entity = new Mock();
entity.Setup(e => e.ExportForPromotionAsync(42, It.IsAny(), It.IsAny()))
.ReturnsAsync(new PromotionBundleDTO { EntityTypeCode = "MENU", OriginEntityId = 42 });
entity.Setup(e => e.ValidateForPromotionAsync(It.IsAny(), It.IsAny()))
.ReturnsAsync(PromotionValidationResultDTO.Ok());
var sp = ServiceProviderWith("MENU", entity.Object);
var bll = new PromotionBLL(sp, Mock.Of(), Mock.Of(), NullLogger.Instance);
var bundle = await bll.BuildExportBundleAsync("MENU", 42, Login(), CancellationToken.None);
Assert.Equal("MENU", bundle.EntityTypeCode);
Assert.Equal(42, bundle.OriginEntityId);
}
[Fact]
public async Task BuildPackageAsync_NoItems_Throws()
{
var bll = new PromotionBLL(
new ServiceCollection().BuildServiceProvider(), Mock.Of(), Mock.Of(), NullLogger.Instance);
await Assert.ThrowsAsync(() =>
bll.BuildPackageAsync(new List(), "IMPSYS-PARTNERX", Login(), CancellationToken.None));
}
[Fact]
public async Task BuildPackageAsync_ThenOpenPackageAsync_RoundTripsTheManifest()
{
// A fake "crypto" that just passes bytes through unchanged, so this test isolates
// PromotionBLL's own manifest-wrapping/serialization logic from real cryptography
// (covered separately in PromotionPackageCryptoTests).
var crypto = new Mock();
byte[]? capturedPlaintext = null;
crypto.Setup(c => c.EncryptPackageAsync(It.IsAny(), "IMPSYS-PARTNERX", It.IsAny(), It.IsAny()))
.Callback((pt, _, _, _) => capturedPlaintext = pt)
.ReturnsAsync(() => capturedPlaintext!);
crypto.Setup(c => c.DecryptPackageAsync(It.IsAny(), It.IsAny()))
.ReturnsAsync(() => capturedPlaintext!);
var bll = new PromotionBLL(
new ServiceCollection().BuildServiceProvider(), crypto.Object, Mock.Of(),
NullLogger.Instance);
var items = new List
{
new() { EntityTypeCode = "MENU", OriginEntityId = 42, OriginEnvironmentCode = "CLIENT-ACME" },
new() { EntityTypeCode = "PORTLETTYPE", OriginEntityId = 7, OriginEnvironmentCode = "CLIENT-ACME" }
};
var packageBytes = await bll.BuildPackageAsync(items, "IMPSYS-PARTNERX", Login(), CancellationToken.None);
var manifest = await bll.OpenPackageAsync(packageBytes, CancellationToken.None);
Assert.Equal(2, manifest.Items.Count);
Assert.Equal("MENU", manifest.Items[0].EntityTypeCode);
Assert.Equal(42, manifest.Items[0].OriginEntityId);
Assert.Equal("PORTLETTYPE", manifest.Items[1].EntityTypeCode);
Assert.Equal("CLIENT-ACME", manifest.OriginEnvironmentCode);
}
[Fact]
public async Task GetSelectListPromotableEntityType_ReturnsDalResultAsJson()
{
var entityTypeDal = new Mock();
entityTypeDal.Setup(d => d.GetSelectList(It.IsAny(), It.IsAny()))
.ReturnsAsync(new[]
{
new FrameworkDAL.DTO.Promotion.PromotableEntityTypeDTO { EntityTypeCode = "MENU", EntityTypeName = "Menu" }
});
var bll = new PromotionBLL(
new ServiceCollection().BuildServiceProvider(), Mock.Of(),
entityTypeDal.Object, NullLogger.Instance);
var json = await bll.GetSelectListPromotableEntityType(Login(), CancellationToken.None);
Assert.Contains("MENU", json);
Assert.Contains("Menu", json);
}
}