using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FrameworkBLL.RoleVsMenu;
using FrameworkDAL.CustomCode.RoleVsMenu;
using FrameworkDAL.DTO.RoleVsMenu;
using FrameworkDAL.Query.RoleVsMenu;
using GB5Shared.DTO.Framework.Login;
using GB5Shared.QueryExecutor;
using Moq;
using Xunit;
namespace FrameworkTests;
///
/// RoleVsMenuBLL.Save's fixed diff-based shape, exercised for the "no new rows" case so
/// AutoNumber (a concrete class with an IOptionsSnapshot dependency, awkward to construct
/// in a unit test) never needs to be touched — GetNumberAsync is only called when at least
/// one incoming (RoleId, MenuId) pair is genuinely new, which this test avoids by design.
/// The new-row/AutoNumber path is a known, explicitly-acknowledged test gap, not silently
/// skipped: covering it would need either a real AutoNumber (IOptionsSnapshot
/// + IQueryExecutor round-trip) or refactoring AutoNumber behind an interface, neither of
/// which is in scope for this fix.
///
public class RoleVsMenuDiffSaveTests
{
private static LoginDTO Login() => new() { ClientId = -1, UserId = -1 };
[Fact]
public async Task Save_ExistingMenuOmittedFromIncomingList_IsDeleted_NotSilentlyKept()
{
var existing = new List
{
new() { RoleVsMenuId = 100, RoleVsMenuMenuId = 501 },
new() { RoleVsMenuId = 101, RoleVsMenuMenuId = 502 }, // omitted from the incoming save below
};
var dal = new Mock();
dal.Setup(x => x.GetExistingForModule(9, 3, It.IsAny())).ReturnsAsync(existing);
List<(string sql, object param)>? captured = null;
var qe = new Mock();
qe.Setup(x => x.ExecuteInTransactionAsync(It.IsAny(), It.IsAny>()))
.Callback>((_, statements) => captured = statements.ToList())
.Returns(Task.CompletedTask);
var bll = new RoleVsMenuBLL(dal.Object, null!, qe.Object);
var incoming = new List
{
new() { RoleVsMenuRoleId = 9, RoleVsMenuMenuId = 501, RoleVsMenuSlNo = 1, RoleVsMenuAllow = "111111" },
};
await bll.Save(ModuleId: 3, PageId: -1, incoming, Login());
Assert.NotNull(captured);
var deleteStatement = Assert.Single(captured!, s => s.sql == RoleVsMenuQB.DELETE_ROLEVSMENU_BY_IDS);
var ids = (IEnumerable)deleteStatement.param.GetType().GetProperty("Ids")!.GetValue(deleteStatement.param)!;
Assert.Equal(new[] { 101 }, ids);
var updateStatement = Assert.Single(captured!, s => s.sql == RoleVsMenuQB.UPDATE_ROLEVSMENU);
Assert.Equal(100, ((RoleVsMenuDTO)updateStatement.param).RoleVsMenuId);
Assert.DoesNotContain(captured!, s => s.sql == RoleVsMenuQB.SAVE_ROLEVSMENU);
}
[Fact]
public async Task Save_ExistingMenu_ReusesSameRoleVsMenuId_InsteadOfDeleteAndReinsert()
{
// The bug this fix targets: the old Save() reassigned a brand-new ROLEVSMENUID to
// every row on every save, destroying CREATEDON/audit history even for a row whose
// (RoleId, MenuId) didn't change. Confirms the update path keeps the original id.
var existing = new List { new() { RoleVsMenuId = 777, RoleVsMenuMenuId = 501 } };
var dal = new Mock();
dal.Setup(x => x.GetExistingForModule(9, 3, It.IsAny())).ReturnsAsync(existing);
List<(string sql, object param)>? captured = null;
var qe = new Mock();
qe.Setup(x => x.ExecuteInTransactionAsync(It.IsAny(), It.IsAny>()))
.Callback>((_, statements) => captured = statements.ToList())
.Returns(Task.CompletedTask);
var bll = new RoleVsMenuBLL(dal.Object, null!, qe.Object);
var incoming = new List
{
new() { RoleVsMenuId = 0, RoleVsMenuRoleId = 9, RoleVsMenuMenuId = 501, RoleVsMenuSlNo = 5, RoleVsMenuAllow = "000000" },
};
await bll.Save(ModuleId: 3, PageId: -1, incoming, Login());
Assert.Equal(777, incoming[0].RoleVsMenuId);
Assert.DoesNotContain(captured!, s => s.sql == RoleVsMenuQB.DELETE_ROLEVSMENU_BY_IDS);
}
}