using GB5Shared.DTO.Framework.Login;
using GB5Shared.EventLogPublish;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using SwBLL.ChangeRequest;
using SwBLL.ClientDatabase;
using SwBLL.DbServer;
using SwBLL.DdlScript;
using SwBLL.Provisioning;
using SwBLL.UserResourceRole;
using SwDAL.CustomCode.ChangeRequest;
using SwDAL.DTO.ChangeRequest;
using SwDAL.DTO.ClientDatabase;
using SwDAL.DTO.DbServer;
using SwDAL.DTO.DdlScript;
using SwDAL.Enums;
using Xunit;
namespace SwTests;
///
/// Covers QueueExecutionAsync/ProcessQueuedExecutionsAsync (tracker §37) — the background
/// alternative to Execute() built because §36.13's own live proof found the inline
/// foreach-over-scripts loop can run for several minutes on a large batch and is tied to the
/// calling HTTP connection's own CancellationToken. These tests mock IChangeRequestDAL directly
/// so none of them touch a real database.
///
public class ChangeRequestBLLAsyncExecutionTests
{
private static (ChangeRequestBLL Svc, Mock Dal, Mock ClientDatabaseBLL)
BuildService()
{
var dal = new Mock();
var clientDatabaseBLL = new Mock();
var dbServerBLL = new Mock();
var targetDbExecutor = new Mock();
var ddlScriptBLL = new Mock();
var provisioner = new Mock();
dal.Setup(d => d.UpdateExecutionStatusAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(Task.CompletedTask);
dal.Setup(d => d.AppendTimeline(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(Task.CompletedTask);
dal.Setup(d => d.UpdateStatus(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(Task.CompletedTask);
dal.Setup(d => d.GetLinkedApprovedDdlScripts(It.IsAny(), It.IsAny(), It.IsAny()))
.ReturnsAsync(new List { new() { DdlScriptId = 1, SqlScript = "SELECT 1", Sequence = 1 } });
clientDatabaseBLL.Setup(c => c.GetById(It.IsAny(), It.IsAny(), It.IsAny()))
.ReturnsAsync(new ClientDatabaseDTO { ClientDbId = 5, ClientDbCode = "acme01", DatabaseName = "acme01", DbServerId = 1, DatabaseRole = 0, TenantId = 1 });
dbServerBLL.Setup(d => d.GetByIdWithCredentials(It.IsAny(), It.IsAny(), It.IsAny()))
.ReturnsAsync(new DbServerDTO { DbServerId = 1, DbType = 0, HostName = "sql01" });
targetDbExecutor.Setup(t => t.BuildConnectionStringForRoleAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
.ReturnsAsync("conn-str");
targetDbExecutor.Setup(t => t.ExecuteScriptAsync(It.IsAny(), It.IsAny(), It.IsAny()))
.ReturnsAsync(0);
ddlScriptBLL.Setup(d => d.LogExecutionAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
.ReturnsAsync("logged");
var eventLogPublish = new EventLogPublish(null!, NullLogger.Instance);
var svc = new ChangeRequestBLL(
dal.Object, clientDatabaseBLL.Object, dbServerBLL.Object, targetDbExecutor.Object,
ddlScriptBLL.Object, provisioner.Object, new Mock().Object,
eventLogPublish, NullLogger.Instance);
return (svc, dal, clientDatabaseBLL);
}
private static ChangeRequestDTO ApprovedDdlCr(int id = 100) => new()
{
ChangeRequestId = id,
ClientDatabaseId = 5,
CrStatus = ChangeRequestStatus.Approved,
Category = ChangeRequestCategory.SchemaChange,
QueryType = ChangeRequestQueryType.DDL
};
private static LoginDTO TestLogin() => new() { ClientId = 1, UserId = -1 };
[Fact]
public async Task Test_QueueExecutionAsync_NotFound_Throws()
{
var (svc, dal, _) = BuildService();
dal.Setup(d => d.GetById(999, It.IsAny(), It.IsAny()))
.ReturnsAsync((ChangeRequestDTO?)null);
await Assert.ThrowsAsync(
() => svc.QueueExecutionAsync(999, TestLogin(), CancellationToken.None));
dal.Verify(d => d.UpdateExecutionStatusAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
}
[Fact]
public async Task Test_QueueExecutionAsync_NotApproved_Throws_NoExecutionStatusChange()
{
var (svc, dal, _) = BuildService();
var cr = ApprovedDdlCr();
cr.CrStatus = ChangeRequestStatus.Draft;
dal.Setup(d => d.GetById(cr.ChangeRequestId, It.IsAny(), It.IsAny())).ReturnsAsync(cr);
await Assert.ThrowsAsync(
() => svc.QueueExecutionAsync(cr.ChangeRequestId, TestLogin(), CancellationToken.None));
dal.Verify(d => d.UpdateExecutionStatusAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
}
[Fact]
public async Task Test_QueueExecutionAsync_Approved_SetsQueuedAndAppendsTimeline()
{
var (svc, dal, _) = BuildService();
var cr = ApprovedDdlCr();
dal.Setup(d => d.GetById(cr.ChangeRequestId, It.IsAny(), It.IsAny())).ReturnsAsync(cr);
var result = await svc.QueueExecutionAsync(cr.ChangeRequestId, TestLogin(), CancellationToken.None);
Assert.Contains("queued", result, StringComparison.OrdinalIgnoreCase);
// queuedById is stamped with the queuing user's own id (TestLogin().UserId == -1) so
// ExecuteCoreAsync can later resolve the correct acting user for a background-worker
// execution — see ChangeRequestQB.UPDATE_EXECUTION_STATUS's own doc comment.
dal.Verify(d => d.UpdateExecutionStatusAsync(cr.ChangeRequestId, (byte)ChangeRequestExecutionStatus.Queued, It.IsAny(), It.IsAny(), -1), Times.Once);
dal.Verify(d => d.AppendTimeline(cr.ChangeRequestId, CrTimelineEventType.Queued, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once);
}
[Fact]
public async Task Test_ProcessQueuedExecutionsAsync_NoQueuedRows_ReturnsZero_NoWork()
{
var (svc, dal, _) = BuildService();
dal.Setup(d => d.GetQueuedForExecutionAsync(It.IsAny(), It.IsAny()))
.ReturnsAsync(Enumerable.Empty());
var processed = await svc.ProcessQueuedExecutionsAsync(TestLogin(), CancellationToken.None);
Assert.Equal(0, processed);
dal.Verify(d => d.UpdateStatus(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
}
[Fact]
public async Task Test_ProcessQueuedExecutionsAsync_OneQueuedRow_RunsIt_MarksDone()
{
var (svc, dal, _) = BuildService();
var cr = ApprovedDdlCr();
dal.Setup(d => d.GetQueuedForExecutionAsync(It.IsAny(), It.IsAny()))
.ReturnsAsync(new List { cr });
var processed = await svc.ProcessQueuedExecutionsAsync(TestLogin(), CancellationToken.None);
Assert.Equal(1, processed);
// Running set first, Done set last — both real transitions, not skipped.
dal.Verify(d => d.UpdateExecutionStatusAsync(cr.ChangeRequestId, (byte)ChangeRequestExecutionStatus.Running, It.IsAny(), It.IsAny()), Times.Once);
dal.Verify(d => d.UpdateExecutionStatusAsync(cr.ChangeRequestId, (byte)ChangeRequestExecutionStatus.Done, It.IsAny(), It.IsAny()), Times.Once);
dal.Verify(d => d.UpdateExecutionStatusAsync(cr.ChangeRequestId, (byte)ChangeRequestExecutionStatus.Failed, It.IsAny(), It.IsAny()), Times.Never);
// The real CR-status transition Execute() itself performs — proves ExecuteCoreAsync ran.
dal.Verify(d => d.UpdateStatus(cr.ChangeRequestId, (byte)ChangeRequestStatus.Executed, (byte)ChangeRequestStatus.Approved, It.IsAny(), null, It.IsAny(), It.IsAny()), Times.Once);
}
[Fact]
public async Task Test_ProcessQueuedExecutionsAsync_OneRowFails_MarksFailed_DoesNotThrow_ContinuesBatch()
{
var (svc, dal, _) = BuildService();
var failing = ApprovedDdlCr(id: 100);
var succeeds = ApprovedDdlCr(id: 101);
dal.Setup(d => d.GetQueuedForExecutionAsync(It.IsAny(), It.IsAny()))
.ReturnsAsync(new List { failing, succeeds });
// clientDatabaseBLL isn't parameterized per-CR in BuildService(), so force the failure via
// GetLinkedApprovedDdlScripts throwing only for the first CR's id.
dal.Setup(d => d.GetLinkedApprovedDdlScripts(failing.ChangeRequestId, It.IsAny(), It.IsAny()))
.ThrowsAsync(new InvalidOperationException("boom"));
var processed = await svc.ProcessQueuedExecutionsAsync(TestLogin(), CancellationToken.None);
Assert.Equal(1, processed); // only the second CR counts as processed
dal.Verify(d => d.UpdateExecutionStatusAsync(failing.ChangeRequestId, (byte)ChangeRequestExecutionStatus.Failed, It.IsAny(), It.IsAny()), Times.Once);
dal.Verify(d => d.UpdateExecutionStatusAsync(succeeds.ChangeRequestId, (byte)ChangeRequestExecutionStatus.Done, It.IsAny(), It.IsAny()), Times.Once);
}
}