using System.Collections.Concurrent;
using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Entity;
using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Idempotency;
using Xunit;
using Xunit.Abstractions;
namespace Admin.NET.Plugin.AiDOP.Tests.S5.MaterialWarehouse;
///
/// Phase 4A 幂等并发测试(同进程;InMemory store 模拟 tenant+domain+FBILLNO 唯一占位)。
/// **可现在证明**:重复 FBILLNO 行为稳定、四态区分、duplicate-key 回读既有、tenant/domain 隔离。
/// **不能声称**:真实 MySQL DB UNIQUE 实拒 / 事务隔离下竞争(→ Phase 4B)。
///
public class IqcPostingIdempotencyTests
{
private readonly ITestOutputHelper _out;
public IqcPostingIdempotencyTests(ITestOutputHelper output) => _out = output;
private const long Tenant = 797403760988229L;
[Fact]
public async Task Claim_1000Concurrent_SameFbillno_ExactlyOneFirstProcess()
{
const int n = 1000;
var svc = new IqcPostingIdempotencyService(new InMemoryIqcPostingStore());
var bag = new ConcurrentBag();
var tasks = Enumerable.Range(0, n).Select(_ => Task.Run(async () =>
{
var r = await svc.ClaimAsync(Tenant, "8010", "FB-CONC-001");
bag.Add(r.Outcome);
}));
await Task.WhenAll(tasks);
var all = bag.ToList();
var first = all.Count(o => o == IqcPostingClaimOutcome.FirstProcess);
var processing = all.Count(o => o == IqcPostingClaimOutcome.Processing);
_out.WriteLine($"[幂等并发] 总={all.Count} FirstProcess={first} Processing={processing} " +
$"AlreadyProcessed={all.Count(o => o == IqcPostingClaimOutcome.AlreadyProcessed)} " +
$"FailedRetryable={all.Count(o => o == IqcPostingClaimOutcome.FailedRetryable)}");
Assert.Equal(n, all.Count);
Assert.Equal(1, first); // 恰好一路首处理 → 不会二次库存写入
Assert.Equal(n - 1, processing); // 其余全部识别为处理中
}
[Fact]
public async Task Claim_SameFbillno_DifferentTenantOrDomain_AllFirstProcess()
{
var svc = new IqcPostingIdempotencyService(new InMemoryIqcPostingStore());
var a = await svc.ClaimAsync(1L, "8010", "FB-1");
var b = await svc.ClaimAsync(2L, "8010", "FB-1"); // 不同租户
var c = await svc.ClaimAsync(1L, "9010", "FB-1"); // 不同域
Assert.Equal(IqcPostingClaimOutcome.FirstProcess, a.Outcome);
Assert.Equal(IqcPostingClaimOutcome.FirstProcess, b.Outcome); // tenant 隔离
Assert.Equal(IqcPostingClaimOutcome.FirstProcess, c.Outcome); // domain 隔离
}
[Fact]
public async Task Claim_AfterPosted_ReturnsAlreadyProcessed()
{
var svc = new IqcPostingIdempotencyService(new InMemoryIqcPostingStore());
var first = await svc.ClaimAsync(Tenant, "8010", "FB-2");
Assert.Equal(IqcPostingClaimOutcome.FirstProcess, first.Outcome);
first.Posting.PostingStatus = "POSTED"; // 模拟过账完成(内存 store 持同引用)
var second = await svc.ClaimAsync(Tenant, "8010", "FB-2");
Assert.Equal(IqcPostingClaimOutcome.AlreadyProcessed, second.Outcome);
Assert.False(second.IsNew);
}
[Fact]
public async Task Claim_AfterFailed_ReturnsFailedRetryable()
{
var svc = new IqcPostingIdempotencyService(new InMemoryIqcPostingStore());
var first = await svc.ClaimAsync(Tenant, "8010", "FB-3");
first.Posting.PostingStatus = "FAILED";
var second = await svc.ClaimAsync(Tenant, "8010", "FB-3");
Assert.Equal(IqcPostingClaimOutcome.FailedRetryable, second.Outcome);
}
[Fact]
public async Task Store_DuplicateInsert_ReadsBackExisting()
{
var store = new InMemoryIqcPostingStore();
var p1 = new AdoIqcInventoryPosting { TenantId = Tenant, DomainCode = "8010", FbillNo = "FB-4", PostingStatus = "PROCESSING" };
var p2 = new AdoIqcInventoryPosting { TenantId = Tenant, DomainCode = "8010", FbillNo = "FB-4", PostingStatus = "PROCESSING" };
var r1 = await store.TryInsertAsync(p1);
var r2 = await store.TryInsertAsync(p2);
Assert.True(r1.Inserted);
Assert.False(r2.Inserted); // 撞唯一
Assert.Same(p1, r2.Existing); // 回读既有(非先查后插)
}
[Fact]
public async Task Claim_EmptyFbillno_Throws()
{
var svc = new IqcPostingIdempotencyService(new InMemoryIqcPostingStore());
await Assert.ThrowsAsync(() => svc.ClaimAsync(Tenant, "8010", ""));
}
[Theory]
[InlineData("POSTED", IqcPostingClaimOutcome.AlreadyProcessed)]
[InlineData("PROCESSING", IqcPostingClaimOutcome.Processing)]
[InlineData("PENDING", IqcPostingClaimOutcome.Processing)]
[InlineData("FAILED", IqcPostingClaimOutcome.FailedRetryable)]
[InlineData("weird", IqcPostingClaimOutcome.Processing)]
[InlineData(null, IqcPostingClaimOutcome.Processing)]
public void Classify_MapsStatusToOutcome(string status, IqcPostingClaimOutcome expected)
{
Assert.Equal(expected, IqcPostingIdempotencyService.Classify(status));
}
}