using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using FrameworkBLL.Menu;
using FrameworkBLL.MenuGroup;
using FrameworkDAL.CustomCode.MenuGroup;
using FrameworkDAL.DTO.MenuGroup;
using GB5Shared.DTO.Framework.CommonConfig;
using GB5Shared.DTO.Framework.Login;
using GB5Shared.GenerateAutoNumber;
using GB5Shared.QueryExecutor;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;
namespace FrameworkTests;
///
/// Covers the two structural guarantees MenuGroupBLL.GetMenuGroupTreeForSet must hold:
/// PRESENTATIONTYPE = 0 delegates unchanged to IMenuBLL (the byte-for-byte legacy
/// compatibility contract, tested here as "no MenuGroup code runs, the call is forwarded
/// verbatim" since a real end-to-end byte comparison belongs in an integration test against
/// live data, not a unit test), and a misconfigured PRESENTATIONTYPE<>0 set (still at the
/// -1 MenuGroupId default) fails loudly instead of silently rendering an empty tree.
/// Also covers ValidateMenuGroupVisibility's shared/private boundary.
/// AutoNumber/IQueryExecutor/KeyInvalidate are passed null! — none of the paths under test
/// here touch them (only Save/Delete/SaveMenuGroupDetail do).
///
public class MenuGroupBLLTests
{
private static LoginDTO Login(int userId = 7) => new() { ClientId = -1, UserId = userId };
[Fact]
public async Task GetMenuGroupTreeForSet_PresentationTypeZero_DelegatesToMenuBLL_Unchanged()
{
var dal = new Mock();
dal.Setup(x => x.GetMenuSetPresentationInfo(55, It.IsAny(), It.IsAny()))
.ReturnsAsync(new MenuSetPresentationInfoDTO { MenuSetId = 55, ModuleId = 3, PresentationType = 0, MenuGroupId = -1 });
var menuBLL = new Mock();
menuBLL.Setup(x => x.GetMenuForUserModule(7, 3, It.IsAny()))
.ReturnsAsync("LEGACY_JSON_UNCHANGED");
var bll = new MenuGroupBLL(dal.Object, menuBLL.Object, null!, null!, null!);
var result = await bll.GetMenuGroupTreeForSet(55, Login(), CancellationToken.None);
Assert.Equal("LEGACY_JSON_UNCHANGED", result);
menuBLL.Verify(x => x.GetMenuForUserModule(7, 3, It.IsAny()), Times.Once);
// No MenuGroup flat-list/leaf-resolution call for a legacy set — confirms no new
// code executes on this path, not just that the output happens to match.
dal.Verify(x => x.GetMenuGroupFlatList(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
}
[Fact]
public async Task GetMenuGroupTreeForSet_NonLegacySetStillAtDefaultMenuGroupId_ThrowsRatherThanRenderingEmptyTree()
{
var dal = new Mock();
dal.Setup(x => x.GetMenuSetPresentationInfo(55, It.IsAny(), It.IsAny()))
.ReturnsAsync(new MenuSetPresentationInfoDTO { MenuSetId = 55, ModuleId = 3, PresentationType = 1, MenuGroupId = -1 });
var bll = new MenuGroupBLL(dal.Object, Mock.Of(), null!, null!, null!);
await Assert.ThrowsAsync(() => bll.GetMenuGroupTreeForSet(55, Login(), CancellationToken.None));
}
[Fact]
public async Task GetMenuGroupTreeForSet_UnknownMenuSetId_Throws()
{
var dal = new Mock();
dal.Setup(x => x.GetMenuSetPresentationInfo(999, It.IsAny(), It.IsAny()))
.ReturnsAsync((MenuSetPresentationInfoDTO?)null);
var bll = new MenuGroupBLL(dal.Object, Mock.Of(), null!, null!, null!);
await Assert.ThrowsAsync(() => bll.GetMenuGroupTreeForSet(999, Login(), CancellationToken.None));
}
[Fact]
public async Task ValidateMenuGroupVisibility_SharedGroup_IsAssignableFromAnyMenuSet()
{
var dal = new Mock();
dal.Setup(x => x.GetMenuGroupById(10, It.IsAny(), It.IsAny()))
.ReturnsAsync(new MenuGroupDTO { MenuGroupId = 10, MenuSetId = null });
var bll = new MenuGroupBLL(dal.Object, Mock.Of(), null!, null!, null!);
await bll.ValidateMenuGroupVisibility(10, callerMenuSetId: 999, Login(), CancellationToken.None);
// No throw = pass.
}
[Fact]
public async Task ValidateMenuGroupVisibility_PrivateGroup_OwnSet_IsAllowed()
{
var dal = new Mock();
dal.Setup(x => x.GetMenuGroupById(10, It.IsAny(), It.IsAny()))
.ReturnsAsync(new MenuGroupDTO { MenuGroupId = 10, MenuSetId = 55 });
var bll = new MenuGroupBLL(dal.Object, Mock.Of(), null!, null!, null!);
await bll.ValidateMenuGroupVisibility(10, callerMenuSetId: 55, Login(), CancellationToken.None);
}
[Fact]
public async Task ValidateMenuGroupVisibility_PrivateGroup_AnotherSet_IsRejected()
{
var dal = new Mock();
dal.Setup(x => x.GetMenuGroupById(10, It.IsAny(), It.IsAny()))
.ReturnsAsync(new MenuGroupDTO { MenuGroupId = 10, MenuSetId = 55 });
var bll = new MenuGroupBLL(dal.Object, Mock.Of(), null!, null!, null!);
await Assert.ThrowsAsync(
() => bll.ValidateMenuGroupVisibility(10, callerMenuSetId: 999, Login(), CancellationToken.None));
}
[Fact]
public async Task SaveMenuGroupDetail_MultipleNewLeaves_EachGetsADistinctSequentialId()
{
// Regression test for a real bug caught while wiring MenuSetBLL.SaveMenuSetDetail:
// SaveMenuGroupDetail never actually called AutoNumber for new leaves, so every new
// leaf kept MenuGroupDetailId == 0 (a PK collision the moment there was more than one
// new leaf in a save). AutoNumber is a concrete class, but both of its own
// dependencies (IQueryExecutor, IOptionsSnapshot) are interfaces, so a
// real AutoNumber can be constructed here instead of needing null!.
var dal = new Mock();
dal.Setup(x => x.GetMenuGroupById(10, It.IsAny(), It.IsAny()))
.ReturnsAsync(new MenuGroupDTO { MenuGroupId = 10, MenuSetId = null });
var qe = new Mock();
// AutoNumber.GetNumberAsync reads results[0] (a long — AutoId's SQL type) as the
// post-increment AUTOID and derives firstNumber = newAutoId - noOfId (+
// ServerConfigOffset, 0 here) — reserving 2 ids starting at 100 means the
// UPDATE ... SET AUTOID = AUTOID + 2 returns 102.
qe.Setup(x => x.QueryAsync(
It.IsAny(), It.IsAny(), It.IsAny