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 ChangeRequestBLL.Execute()'s new ClientProvisioning pre-step — the branch that creates
/// the physical database (via IClientDatabaseProvisioner) before falling through to the existing,
/// unmodified DDL-execution path. Every case here mocks IClientDatabaseProvisioner directly, so
/// none of these tests touch a real database or Vault — they only prove Execute() picks the right
/// provisioner method for the CR's ProvisioningMode and validates the required fields first.
///
public class ChangeRequestBLLProvisioningTests
{
private static (ChangeRequestBLL Svc, Mock Dal, Mock ClientDatabaseBLL,
Mock Provisioner) 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.GetLinkedApprovedDdlScripts(It.IsAny(), It.IsAny(), It.IsAny()))
.ReturnsAsync(Enumerable.Empty());
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.AppendTimeline(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(Task.CompletedTask);
clientDatabaseBLL.Setup(c => c.GetById(It.IsAny(), It.IsAny(), It.IsAny()))
.ReturnsAsync(new ClientDatabaseDTO { ClientDbId = 5, ClientDbCode = "acme01", DatabaseName = "acme01", DbServerId = 1, DatabaseRole = 0 });
clientDatabaseBLL.Setup(c => c.UpdateStatusAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
.ReturnsAsync("updated");
dbServerBLL.Setup(d => d.GetByIdWithCredentials(It.IsAny(), It.IsAny(), It.IsAny()))
.ReturnsAsync(new DbServerDTO { DbServerId = 1, DbType = 0, HostName = "sql01" });
targetDbExecutor.Setup(t => t.BuildConnectionString(It.IsAny(), It.IsAny()))
.Returns("conn-str");
provisioner.Setup(p => p.CreateFromSnapshotAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(Task.CompletedTask);
provisioner.Setup(p => p.CreateFromScriptsAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(Task.CompletedTask);
// EventLogPublish.PublishEventLogAsync catches and logs every exception internally
// (never throws) — a null DaprClient is a safe test double here, it just makes the
// publish attempt fail silently (logged as a warning), exactly like a real Dapr sidecar
// being unreachable would in production. None of these tests assert on the publish itself.
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, provisioner);
}
private static ChangeRequestDTO ApprovedProvisioningCr(
ProvisioningMode? mode = null, string? templateBackupRef = null, int? baselinePackageId = null) => new()
{
ChangeRequestId = 100,
ClientDatabaseId = 5,
CrStatus = ChangeRequestStatus.Approved,
Category = ChangeRequestCategory.ClientProvisioning,
QueryType = ChangeRequestQueryType.Other,
ProvisioningMode = mode,
TemplateBackupRef = templateBackupRef,
BaselineUpgradePackageId = baselinePackageId
};
private static LoginDTO TestLogin() => new() { ClientId = 42, UserId = -1 };
[Fact]
public async Task Test_Execute_ClientProvisioning_MissingProvisioningMode_Throws()
{
var (svc, dal, _, provisioner) = BuildService();
var cr = ApprovedProvisioningCr(mode: null);
dal.Setup(d => d.GetById(cr.ChangeRequestId, It.IsAny(), It.IsAny())).ReturnsAsync(cr);
await Assert.ThrowsAsync(() =>
svc.Execute(cr.ChangeRequestId, TestLogin(), CancellationToken.None));
provisioner.Verify(p => p.CreateFromSnapshotAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
provisioner.Verify(p => p.CreateFromScriptsAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
}
[Fact]
public async Task Test_Execute_ClientProvisioning_FromSnapshot_MissingTemplateBackupRef_Throws()
{
var (svc, dal, _, provisioner) = BuildService();
var cr = ApprovedProvisioningCr(mode: ProvisioningMode.FromSnapshot, templateBackupRef: null);
dal.Setup(d => d.GetById(cr.ChangeRequestId, It.IsAny(), It.IsAny())).ReturnsAsync(cr);
await Assert.ThrowsAsync(() =>
svc.Execute(cr.ChangeRequestId, TestLogin(), CancellationToken.None));
provisioner.Verify(p => p.CreateFromSnapshotAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
}
[Fact]
public async Task Test_Execute_ClientProvisioning_FromSnapshot_CallsProvisionerAndActivatesClientDb()
{
var (svc, dal, clientDatabaseBLL, provisioner) = BuildService();
var cr = ApprovedProvisioningCr(mode: ProvisioningMode.FromSnapshot, templateBackupRef: "acme-template.bak");
dal.Setup(d => d.GetById(cr.ChangeRequestId, It.IsAny(), It.IsAny())).ReturnsAsync(cr);
await svc.Execute(cr.ChangeRequestId, TestLogin(), CancellationToken.None);
provisioner.Verify(p => p.CreateFromSnapshotAsync(
It.Is(c => c.ClientDbId == 5), It.IsAny(), "acme-template.bak",
It.IsAny(), It.IsAny()), Times.Once);
provisioner.Verify(p => p.CreateFromScriptsAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
clientDatabaseBLL.Verify(c => c.UpdateStatusAsync(5, (byte)ClientDbStatus.Active, It.IsAny(), It.IsAny()), Times.Once);
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_Execute_ClientProvisioning_FromScripts_MissingBaselinePackageId_Throws()
{
var (svc, dal, _, provisioner) = BuildService();
var cr = ApprovedProvisioningCr(mode: ProvisioningMode.FromScripts, baselinePackageId: null);
dal.Setup(d => d.GetById(cr.ChangeRequestId, It.IsAny(), It.IsAny())).ReturnsAsync(cr);
await Assert.ThrowsAsync(() =>
svc.Execute(cr.ChangeRequestId, TestLogin(), CancellationToken.None));
provisioner.Verify(p => p.CreateFromScriptsAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
}
[Fact]
public async Task Test_Execute_ClientProvisioning_FromScripts_CallsProvisionerWithBaselinePackage()
{
var (svc, dal, clientDatabaseBLL, provisioner) = BuildService();
var cr = ApprovedProvisioningCr(mode: ProvisioningMode.FromScripts, baselinePackageId: 77);
dal.Setup(d => d.GetById(cr.ChangeRequestId, It.IsAny(), It.IsAny())).ReturnsAsync(cr);
await svc.Execute(cr.ChangeRequestId, TestLogin(), CancellationToken.None);
provisioner.Verify(p => p.CreateFromScriptsAsync(
It.Is(c => c.ClientDbId == 5), It.IsAny(), 77,
It.IsAny(), It.IsAny()), Times.Once);
clientDatabaseBLL.Verify(c => c.UpdateStatusAsync(5, (byte)ClientDbStatus.Active, It.IsAny(), It.IsAny()), Times.Once);
}
[Fact]
public async Task Test_Execute_NonProvisioningCategory_NeverCallsProvisioner()
{
var (svc, dal, _, provisioner) = BuildService();
var cr = new ChangeRequestDTO
{
ChangeRequestId = 200,
ClientDatabaseId = 5,
CrStatus = ChangeRequestStatus.Approved,
Category = ChangeRequestCategory.SchemaChange,
QueryType = ChangeRequestQueryType.DDL
};
dal.Setup(d => d.GetById(cr.ChangeRequestId, It.IsAny(), It.IsAny())).ReturnsAsync(cr);
dal.Setup(d => d.GetLinkedApprovedDdlScripts(cr.ChangeRequestId, It.IsAny(), It.IsAny()))
.ReturnsAsync(new List { new() { DdlScriptId = 1, SqlScript = "SELECT 1" } });
await svc.Execute(cr.ChangeRequestId, TestLogin(), CancellationToken.None);
provisioner.Verify(p => p.CreateFromSnapshotAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
provisioner.Verify(p => p.CreateFromScriptsAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
}
}