using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using FrameworkBLL.GOP;
using FrameworkBLL.GOP.Worker;
using FrameworkDAL.CustomCode.GOP;
using GB5Shared.DTO.Framework.Login;
using GB5Shared.DTO.GOP;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
namespace FrameworkTests;
///
/// Regression coverage for a real bug found live on 2026-08-21 verifying Metadata Promotion's
/// first-ever GOP execution on GB5DEMO: GopExecutionPipeline.ExecuteAsync takes an explicit
/// `clientId` (the execution's OWN tenant, correctly used by GetExecutionById/UpdateExecutionStatus
/// etc.) alongside a `loginDTO` used purely for DB connection purposes. GopFlowDAL.GetSnapshotSteps/
/// GetSnapshotStepEdges instead filtered by loginDTO.ClientId — fine for a normal per-tenant flow
/// where the two happen to match, but wrong for exactly Promotion's own design: a background
/// worker's LoginDTO carries the real business tenant (e.g. GB5DEMO's -1399999868, built from
/// MSERVERCONFIG), while the queued execution itself belongs to the framework-level ClientId=-1
/// tenant. The mismatch made GetSnapshotSteps return zero rows, which the pipeline's own "no steps
/// found" branch silently treated as a completed Success — an execution that never touched a single
/// node, approval gate included, reporting as if it had worked perfectly.
///
public class GopExecutionPipelineTenantIdTests
{
private const int ExecutionTenantId = -1; // the execution's own ClientId (e.g. Promotion's framework-level tenant)
private const int WorkerConnectionTenantId = -1399999868; // the worker's LoginDTO.ClientId (GB5DEMO's real business tenant)
private static GopExecutionHeaderDTO FreshHeader() => new()
{
ClientId = ExecutionTenantId,
ExecutionId = 1033,
FlowId = 42,
FlowCode = "PromotionIngest",
SnapshotVersion = "v1",
Status = "Queued",
SourcePayloadJson = "{}",
CurrentNodeId = null
};
private static GopFlowSnapshotStepDTO ApprovalRootStep() => new()
{
ClientId = ExecutionTenantId,
NodeId = 1,
NodeCode = "approve",
NodeType = "Approval",
AssignedRole = "PROMOTIONREVIEWER"
};
[Fact]
public async Task ExecuteAsync_WhenExecutionTenantDiffersFromLoginTenant_StillLoadsAndTraversesSteps()
{
var queueDal = new Mock();
var flowDal = new Mock();
var hubNotifier = new Mock();
var header = FreshHeader();
queueDal.Setup(q => q.GetExecutionById(ExecutionTenantId, header.ExecutionId, It.IsAny()))
.ReturnsAsync(header);
// The regression: GetSnapshotSteps must be called with the EXECUTION's tenant
// (ExecutionTenantId), not the connecting LoginDTO's ClientId (WorkerConnectionTenantId).
// Wired to return the real step ONLY for the correct tenant, and empty for the wrong one —
// so if the bug reappears, the pipeline would immediately go down the "no steps found" path.
flowDal.Setup(f => f.GetSnapshotSteps(ExecutionTenantId, header.FlowId, header.SnapshotVersion, It.IsAny()))
.ReturnsAsync(new List { ApprovalRootStep() });
flowDal.Setup(f => f.GetSnapshotSteps(WorkerConnectionTenantId, header.FlowId, header.SnapshotVersion, It.IsAny()))
.ReturnsAsync(new List()); // what the bug used to return
flowDal.Setup(f => f.GetSnapshotStepEdges(ExecutionTenantId, header.FlowId, header.SnapshotVersion, It.IsAny(), It.IsAny()))
.ReturnsAsync(new List());
var sp = new ServiceCollection().BuildServiceProvider();
var pipeline = new GopExecutionPipeline(
queueDal.Object, flowDal.Object, sp, hubNotifier.Object, NullLogger.Instance);
// loginDTO.ClientId deliberately differs from the execution's own tenant — exactly the
// real GopWorkerService shape (workerLogin.ClientId comes from MSERVERCONFIG, not from the
// queued execution row).
var workerLogin = new LoginDTO { ClientId = WorkerConnectionTenantId, DatabaseName = "GB5DEMO" };
await pipeline.ExecuteAsync(header.ExecutionId, ExecutionTenantId, workerLogin, CancellationToken.None);
// Proves real traversal reached the Approval gate (only possible if GetSnapshotSteps
// actually returned the step) rather than short-circuiting via the "no steps found" branch.
queueDal.Verify(q => q.UpdateExecutionApprovalPending(
ExecutionTenantId, header.ExecutionId, 1, "approve", It.IsAny(),
"PROMOTIONREVIEWER", null, It.IsAny()), Times.Once);
// Never silently marked Success/Failed via the empty-steps branch.
queueDal.Verify(q => q.UpdateExecutionStatus(
It.IsAny(), header.ExecutionId, "Success", It.IsAny(), It.IsAny(), It.IsAny()),
Times.Never);
}
}