using System.Data.Common;
using Dapr.Client;
using EntitlementBLL.Common;
using EntitlementBLL.Exceptions;
using EntitlementBLL.Implementations;
using EntitlementBLL.Interfaces;
using EntitlementDAL.DTOs;
using EntitlementDAL.Enums;
using EntitlementDAL.Interfaces;
using GB5Shared.DTO.Framework.CommonConfig;
using GB5Shared.DTO.Framework.Login;
using GB5Shared.EventLogPublish;
using GB5Shared.GenerateAutoNumber;
using GB5Shared.QueryExecutor;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;
namespace EntitlementTests;
///
/// Covers the new lightweight, Entitlement-owned request/approval mechanism (Thread 8 ยง1.6):
/// submitting a request never mutates anything (ChangePlan/Renew/BetaEnrol/BetaOptOut's real
/// mutation only happens once a GOODBOOKS_ADMIN approves it), and ApproveChangeRequestAsync
/// dispatches to the correct existing mutation method based on RequestType.
///
public class ChangeRequestBLLTests
{
private static (ChangeRequestBLL Svc, Mock ChangeRequestDal, Mock SubscriptionDal,
Mock SubscriptionService, Mock BetaClientBLL) BuildService()
{
var changeRequestDal = new Mock();
var subscriptionDal = new Mock();
var subscriptionService = new Mock();
var betaClientBLL = new Mock();
changeRequestDal
.Setup(d => d.SaveAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(Task.CompletedTask);
changeRequestDal
.Setup(d => d.UpdateReviewAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(Task.CompletedTask);
subscriptionService
.Setup(s => s.ChangePlanAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(Task.CompletedTask);
subscriptionService
.Setup(s => s.RenewAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(Task.CompletedTask);
betaClientBLL
.Setup(b => b.EnrolAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
.ReturnsAsync("ok");
betaClientBLL
.Setup(b => b.OptOutAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
.ReturnsAsync("ok");
var queryExecutor = new Mock();
queryExecutor
.Setup(q => q.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny(),
It.IsAny(), It.IsAny(), It.IsAny()))
.ReturnsAsync(new List { 401 });
queryExecutor.Setup(q => q.BeginTransactionAsync(It.IsAny())).ReturnsAsync((DbTransaction)null!);
queryExecutor.Setup(q => q.CommitAsync(It.IsAny())).Returns(Task.CompletedTask);
queryExecutor.Setup(q => q.RollbackAsync(It.IsAny())).Returns(Task.CompletedTask);
var dbConfig = new Mock>();
dbConfig.Setup(o => o.Value).Returns(new Gb5SystemDTO { DataBaseType = 0 });
var autoNumber = new AutoNumber(queryExecutor.Object, dbConfig.Object);
var loginFactory = new Mock();
loginFactory.Setup(f => f.Create(It.IsAny(), It.IsAny()))
.Returns((int clientId, int userId) => new LoginDTO { ClientId = clientId, UserId = userId });
var eventLog = new EventLogPublish(new DaprClientBuilder().Build(), NullLogger.Instance);
var svc = new ChangeRequestBLL(
changeRequestDal.Object, subscriptionDal.Object, subscriptionService.Object, betaClientBLL.Object,
autoNumber, queryExecutor.Object, loginFactory.Object, eventLog);
return (svc, changeRequestDal, subscriptionDal, subscriptionService, betaClientBLL);
}
[Fact]
public async Task Test_SubmitChangePlanRequestAsync_SubscriptionNotFound_Throws()
{
var (svc, _, subscriptionDal, _, _) = BuildService();
subscriptionDal.Setup(d => d.GetByIdAsync(999, It.IsAny(), It.IsAny()))
.ReturnsAsync((SubscriptionDTO?)null);
await Assert.ThrowsAsync(
() => svc.SubmitChangePlanRequestAsync(42, 999, 5, 1, CancellationToken.None));
}
[Fact]
public async Task Test_SubmitChangePlanRequestAsync_SubscriptionBelongsToDifferentClient_Throws()
{
var (svc, _, subscriptionDal, _, _) = BuildService();
subscriptionDal.Setup(d => d.GetByIdAsync(10, It.IsAny(), It.IsAny()))
.ReturnsAsync(new SubscriptionDTO { SubscriptionId = 10, ClientId = 999 });
await Assert.ThrowsAsync(
() => svc.SubmitChangePlanRequestAsync(42, 10, 5, 1, CancellationToken.None));
}
[Fact]
public async Task Test_SubmitChangePlanRequestAsync_Valid_InsertsPendingRequest_NoMutationApplied()
{
var (svc, changeRequestDal, subscriptionDal, subscriptionService, _) = BuildService();
subscriptionDal.Setup(d => d.GetByIdAsync(10, It.IsAny(), It.IsAny()))
.ReturnsAsync(new SubscriptionDTO { SubscriptionId = 10, ClientId = 42 });
var changeRequestId = await svc.SubmitChangePlanRequestAsync(42, 10, 5, 1, CancellationToken.None);
Assert.True(changeRequestId > 0);
changeRequestDal.Verify(d => d.SaveAsync(
It.Is(r => r.RequestType == (byte)ChangeRequestTypeEnum.ChangePlan
&& r.RequestStatus == (byte)ChangeRequestStatusEnum.Pending
&& r.SubscriptionId == 10 && r.ProposedPlanId == 5),
It.IsAny(), It.IsAny(), It.IsAny()), Times.Once);
subscriptionService.Verify(s => s.ChangePlanAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
}
[Fact]
public async Task Test_SubmitBetaOptOutRequestAsync_EmptyProgramName_Throws()
{
var (svc, _, _, _, _) = BuildService();
await Assert.ThrowsAsync(
() => svc.SubmitBetaOptOutRequestAsync(42, "", 1, CancellationToken.None));
}
[Fact]
public async Task Test_ApproveChangeRequestAsync_NotPending_Throws()
{
var (svc, changeRequestDal, _, _, _) = BuildService();
changeRequestDal.Setup(d => d.GetByIdAsync(1, It.IsAny(), It.IsAny()))
.ReturnsAsync(new ChangeRequestDTO { ChangeRequestId = 1, RequestStatus = (byte)ChangeRequestStatusEnum.Approved });
await Assert.ThrowsAsync(
() => svc.ApproveChangeRequestAsync(1, 9, null, CancellationToken.None));
}
[Fact]
public async Task Test_ApproveChangeRequestAsync_ChangePlanType_DispatchesToSubscriptionService_AndMarksApproved()
{
var (svc, changeRequestDal, _, subscriptionService, _) = BuildService();
changeRequestDal.Setup(d => d.GetByIdAsync(1, It.IsAny(), It.IsAny()))
.ReturnsAsync(new ChangeRequestDTO
{
ChangeRequestId = 1, ClientId = 42, RequestType = (byte)ChangeRequestTypeEnum.ChangePlan,
RequestStatus = (byte)ChangeRequestStatusEnum.Pending, SubscriptionId = 10, ProposedPlanId = 5
});
var result = await svc.ApproveChangeRequestAsync(1, 9, "looks good", CancellationToken.None);
Assert.NotNull(result);
subscriptionService.Verify(s => s.ChangePlanAsync(10, 5, 9, It.IsAny()), Times.Once);
changeRequestDal.Verify(d => d.UpdateReviewAsync(1, (byte)ChangeRequestStatusEnum.Approved, 9, "looks good", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once);
}
[Fact]
public async Task Test_ApproveChangeRequestAsync_RenewType_DispatchesToSubscriptionServiceRenewAsync()
{
var (svc, changeRequestDal, _, subscriptionService, _) = BuildService();
var proposedValidTill = DateTime.UtcNow.AddYears(1);
changeRequestDal.Setup(d => d.GetByIdAsync(2, It.IsAny(), It.IsAny()))
.ReturnsAsync(new ChangeRequestDTO
{
ChangeRequestId = 2, ClientId = 42, RequestType = (byte)ChangeRequestTypeEnum.Renew,
RequestStatus = (byte)ChangeRequestStatusEnum.Pending, SubscriptionId = 10, ProposedValidTill = proposedValidTill
});
await svc.ApproveChangeRequestAsync(2, 9, null, CancellationToken.None);
subscriptionService.Verify(s => s.RenewAsync(10, proposedValidTill, 9, It.IsAny()), Times.Once);
}
[Fact]
public async Task Test_ApproveChangeRequestAsync_BetaEnrolType_DispatchesToBetaClientBLLEnrolAsync()
{
var (svc, changeRequestDal, _, _, betaClientBLL) = BuildService();
var proposedValidTill = DateTime.UtcNow.AddMonths(6);
changeRequestDal.Setup(d => d.GetByIdAsync(3, It.IsAny(), It.IsAny()))
.ReturnsAsync(new ChangeRequestDTO
{
ChangeRequestId = 3, ClientId = 42, RequestType = (byte)ChangeRequestTypeEnum.BetaEnrol,
RequestStatus = (byte)ChangeRequestStatusEnum.Pending, BetaProgramName = "EARLY_ACCESS", ProposedValidTill = proposedValidTill
});
await svc.ApproveChangeRequestAsync(3, 9, null, CancellationToken.None);
betaClientBLL.Verify(b => b.EnrolAsync(42, "EARLY_ACCESS", It.IsAny(), proposedValidTill, It.IsAny(), It.IsAny()), Times.Once);
}
[Fact]
public async Task Test_ApproveChangeRequestAsync_BetaOptOutType_DispatchesToBetaClientBLLOptOutAsync()
{
var (svc, changeRequestDal, _, _, betaClientBLL) = BuildService();
changeRequestDal.Setup(d => d.GetByIdAsync(4, It.IsAny(), It.IsAny()))
.ReturnsAsync(new ChangeRequestDTO
{
ChangeRequestId = 4, ClientId = 42, RequestType = (byte)ChangeRequestTypeEnum.BetaOptOut,
RequestStatus = (byte)ChangeRequestStatusEnum.Pending, BetaProgramName = "EARLY_ACCESS"
});
await svc.ApproveChangeRequestAsync(4, 9, null, CancellationToken.None);
betaClientBLL.Verify(b => b.OptOutAsync(42, "EARLY_ACCESS", It.IsAny(), It.IsAny()), Times.Once);
}
[Fact]
public async Task Test_RejectChangeRequestAsync_Valid_MarksRejected_NoMutationApplied()
{
var (svc, changeRequestDal, _, subscriptionService, betaClientBLL) = BuildService();
changeRequestDal.Setup(d => d.GetByIdAsync(1, It.IsAny(), It.IsAny()))
.ReturnsAsync(new ChangeRequestDTO
{
ChangeRequestId = 1, ClientId = 42, RequestType = (byte)ChangeRequestTypeEnum.ChangePlan,
RequestStatus = (byte)ChangeRequestStatusEnum.Pending, SubscriptionId = 10, ProposedPlanId = 5
});
var result = await svc.RejectChangeRequestAsync(1, 9, "not now", CancellationToken.None);
Assert.NotNull(result);
changeRequestDal.Verify(d => d.UpdateReviewAsync(1, (byte)ChangeRequestStatusEnum.Rejected, 9, "not now", It.IsAny(), It.IsAny(), It.IsAny()), Times.Once);
subscriptionService.Verify(s => s.ChangePlanAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
betaClientBLL.Verify(b => b.EnrolAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
}
}