using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Inventory;
using Xunit;
namespace Admin.NET.Plugin.AiDOP.Tests.S5.MaterialWarehouse;
///
/// C5 并发一致性语义测试(Phase 5C;事务/锁设计语义单测,**非 MySQL 并发实证**——Phase 5B)。
/// 验证「Load(锁内快照)+Guard+Write 同锁边界」修复 -60 竞态,及首建/批次/租户隔离。
///
public class InventoryConcurrencyTests
{
private const long T1 = 797403760988229L;
private const long T2 = 824585161322565L;
private static InventoryTransactionCommand Cmd(string tt, decimal qty, long tenant = T1, string item = "IT-1", string loc = "L1", string lot = "", string refs = "")
=> new InventoryTransactionCommand
{
TenantId = tenant, DomainCode = "8010", PostingId = 1, TransactionGroupId = 100, Seq = 1,
TransType = tt, ChangeQty = qty, UmConversion = 1m, ItemNum = item, Location = loc, LotSerial = lot, Refs = refs, User = "t",
};
private static InventoryTransactionService Svc(FakeInventoryDatabase db)
=> new InventoryTransactionService(new FakeInventoryUnitOfWorkFactory(db));
private static Task RunConcurrently(InventoryTransactionService svc, params InventoryTransactionCommand[][] batches)
=> Task.WhenAll(batches.Select(b => Task.Run(() => svc.PostAsync(b))));
// Case A:库存 100,两并发各出 80 → 一成功一守卫失败,最终 20,不为 -60
[Fact]
public async Task CaseA_TwoConcurrentOut80_From100_OneFails_NoNegative()
{
var db = new FakeInventoryDatabase();
db.SeedStock(T1, "8010", "IT-1", "L1", 100m);
var svc = Svc(db);
var results = await RunConcurrently(svc, new[] { Cmd("iss-x", -80m) }, new[] { Cmd("iss-x", -80m) });
Assert.Equal(1, results.Count(r => r.Outcome == InventoryPostingOutcome.Success));
Assert.Equal(1, results.Count(r => r.Outcome == InventoryPostingOutcome.ValidationFailure));
var det = db.LocationDetail[FakeInventoryDatabase.DetK(T1, "8010", "IT-1", "L1", "", "")];
Assert.Equal(20m, det.QtyOnHand); // 只出一次 → 20
Assert.Equal(20m, det.AvailStatusQty);
Assert.True(det.AvailStatusQty >= 0m); // 绝不为 -60
Assert.Equal(20m, db.InvMaster[FakeInventoryDatabase.InvK(T1, "8010", "IT-1", "L1")].QtyOnHand);
}
// Case B:两并发首次 rct-po-ins 同一自然键 → 仅一行、数量正确累计
[Fact]
public async Task CaseB_TwoConcurrentFirstInsert_SameKey_OneRow_SumCorrect()
{
var db = new FakeInventoryDatabase();
db.SeedMaster(T1, "8010", "IT-1", "L1"); // 主数据在,无库存行
var svc = Svc(db);
var results = await RunConcurrently(svc, new[] { Cmd("rct-po-ins", 50m) }, new[] { Cmd("rct-po-ins", 50m) });
Assert.All(results, r => Assert.Equal(InventoryPostingOutcome.Success, r.Outcome));
Assert.Equal(1, db.InvMaster.Count); // 仅一行自然键
Assert.Equal(100m, db.InvMaster[FakeInventoryDatabase.InvK(T1, "8010", "IT-1", "L1")].QtyOnHand); // 50+50
Assert.Equal(100m, db.InvMaster[FakeInventoryDatabase.InvK(T1, "8010", "IT-1", "L1")].Assay); // rct-po-ins 入质检
Assert.Equal(1, db.LocationDetail.Count);
Assert.Equal(100m, db.LocationDetail[FakeInventoryDatabase.DetK(T1, "8010", "IT-1", "L1", "", "")].QtyOnHand);
}
// Case C:同 item/location 不同 lot → 两批次明细独立、互不污染(InvMaster 共享正确累计)
[Fact]
public async Task CaseC_SameItemLoc_DifferentLot_IndependentDetails()
{
var db = new FakeInventoryDatabase();
db.SeedMaster(T1, "8010", "IT-1", "L1");
var svc = Svc(db);
var results = await RunConcurrently(svc,
new[] { Cmd("rct-po-ins", 30m, lot: "LOT-A") },
new[] { Cmd("rct-po-ins", 40m, lot: "LOT-B") });
Assert.All(results, r => Assert.Equal(InventoryPostingOutcome.Success, r.Outcome));
Assert.Equal(2, db.LocationDetail.Count); // 两 lot 各一行
Assert.Equal(30m, db.LocationDetail[FakeInventoryDatabase.DetK(T1, "8010", "IT-1", "L1", "LOT-A", "")].QtyOnHand);
Assert.Equal(40m, db.LocationDetail[FakeInventoryDatabase.DetK(T1, "8010", "IT-1", "L1", "LOT-B", "")].QtyOnHand);
Assert.Equal(1, db.InvMaster.Count); // InvMaster 不含批次 → 共享一行
Assert.Equal(70m, db.InvMaster[FakeInventoryDatabase.InvK(T1, "8010", "IT-1", "L1")].QtyOnHand); // 30+40
}
// Case D:不同 tenant 同 item/location → 互不污染、各自独立行
[Fact]
public async Task CaseD_DifferentTenant_Isolated()
{
var db = new FakeInventoryDatabase();
db.SeedMaster(T1, "8010", "IT-1", "L1");
db.SeedMaster(T2, "8010", "IT-1", "L1");
var svc = Svc(db);
var results = await RunConcurrently(svc,
new[] { Cmd("rct-po-ins", 50m, tenant: T1) },
new[] { Cmd("rct-po-ins", 50m, tenant: T2) });
Assert.All(results, r => Assert.Equal(InventoryPostingOutcome.Success, r.Outcome));
Assert.Equal(2, db.InvMaster.Count); // 两租户各一行
Assert.Equal(50m, db.InvMaster[FakeInventoryDatabase.InvK(T1, "8010", "IT-1", "L1")].QtyOnHand);
Assert.Equal(50m, db.InvMaster[FakeInventoryDatabase.InvK(T2, "8010", "IT-1", "L1")].QtyOnHand);
}
}