using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using FrameworkDAL.CustomCode.MenuGroup;
using FrameworkDAL.DTO.MenuGroup;
using FrameworkDAL.Query.MenuGroup;
using GB5Shared.DTO.Framework.Login;
using GB5Shared.QueryExecutor;
using Moq;
using Xunit;
namespace FrameworkTests;
///
/// MenuGroupDAL.SaveMenuGroupDetail ports SkillProcessStageMapDAL.Save's diff-save shape
/// (delete-not-in-surviving-ids + per-row insert/update, one ExecuteInTransactionAsync call)
/// for MMENUGROUPDETAIL. IQueryExecutor is mocked (Moq, already a project dependency) so
/// these assert the exact statement list built, without a real DB — the SQL text itself
/// (MenuGroupQB constants) is plain parameterized Dapper SQL with no dialect-specific syntax,
/// so there's nothing dialect-sensitive left to verify against a real connection here.
///
public class MenuGroupDetailDiffSaveTests
{
private static LoginDTO Login() => new() { ClientId = -1, UserId = -1 };
[Fact]
public async Task SaveMenuGroupDetail_MixOfNewUpdateAndOmitted_BuildsDeleteNotInPlusUpsertStatements()
{
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 dal = new MenuGroupDAL(qe.Object);
// MenuGroupDetailId 20 survives (update), 10 is omitted (must be deleted), 0 is new (insert).
var leaves = new List
{
new() { MenuGroupDetailId = 20, MenuId = 501, SortOrder = 1 },
new() { MenuGroupDetailId = 0, MenuId = 502, SortOrder = 2 },
};
await dal.SaveMenuGroupDetail(menuGroupId: 7, leaves, Login(), CancellationToken.None);
Assert.NotNull(captured);
var deleteStatement = Assert.Single(captured!, s => s.sql == MenuGroupQB.DELETE_MENUGROUPDETAIL_NOT_IN);
var survivingIds = Assert.IsAssignableFrom(
deleteStatement.param.GetType().GetProperty("SurvivingIds")!.GetValue(deleteStatement.param))
.Cast().ToList();
Assert.Equal(new[] { 20 }, survivingIds);
Assert.Contains(captured!, s => s.sql == MenuGroupQB.UPDATE_MENUGROUPDETAIL);
Assert.Contains(captured!, s => s.sql == MenuGroupQB.INSERT_MENUGROUPDETAIL);
Assert.Equal(2, captured!.Count(s => s.sql == MenuGroupQB.UPDATE_MENUGROUPDETAIL || s.sql == MenuGroupQB.INSERT_MENUGROUPDETAIL));
}
[Fact]
public async Task SaveMenuGroupDetail_EmptyList_DeletesAllExistingLeaves()
{
// Intentional deviation from SkillProcessStageMapDAL.Save (which no-ops on an empty
// list) — see MenuGroupDAL's own comment: "this group now has zero leaves" is a real
// admin action, not something to silently skip.
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 dal = new MenuGroupDAL(qe.Object);
await dal.SaveMenuGroupDetail(menuGroupId: 7, new List(), Login(), CancellationToken.None);
Assert.NotNull(captured);
var only = Assert.Single(captured!);
Assert.Equal(MenuGroupQB.DELETE_MENUGROUPDETAIL_BY_GROUP, only.sql);
}
[Fact]
public async Task SaveMenuGroupDetail_AllNew_NoDeleteNotInStatement_UsesDeleteAllGuard()
{
// No surviving ids (every incoming row is new) must use the delete-all-by-group form,
// not a NOT IN with an empty list (which is invalid/always-true SQL depending on dialect).
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 dal = new MenuGroupDAL(qe.Object);
await dal.SaveMenuGroupDetail(menuGroupId: 7,
new List { new() { MenuGroupDetailId = 0, MenuId = 900 } },
Login(), CancellationToken.None);
Assert.NotNull(captured);
Assert.Contains(captured!, s => s.sql == MenuGroupQB.DELETE_MENUGROUPDETAIL_BY_GROUP);
Assert.DoesNotContain(captured!, s => s.sql == MenuGroupQB.DELETE_MENUGROUPDETAIL_NOT_IN);
}
}