using System.Data.Common;
using Dapr.Client;
using EntitlementBLL.Auth;
using EntitlementBLL.Legal;
using EntitlementBLL.Options;
using EntitlementDAL.DTOs;
using EntitlementDAL.Enums;
using EntitlementDAL.Interfaces;
using GB5Shared.DirectAction;
using GB5Shared.DTO.DirectAction;
using GB5Shared.ActionProcessor;
using GB5Shared.DTO.Framework.CommonConfig;
using GB5Shared.DTO.Framework.Login;
using GB5Shared.EncryptionHelper;
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 EntitlementBLL.Auth.ClientAuthBLL.LoginAsync's account-lockout wiring and
/// SetClientUserStatusAsync's admin-unlock path — the real fix for the gap DXP shipped with:
/// DXP's MDXPUSER.FAILEDLOGINCOUNT increments on every wrong password and a Status==2 check
/// exists in code, but no threshold is ever read or enforced, so that check never fires. Here,
/// FailedLoginCount increments on a wrong password against an Active account, and once it reaches
/// ClientAccountLockoutOptions.FailedLoginThreshold, STATUS flips to Locked in the SAME update
/// (verified by capturing the exact (status, count) pair passed to
/// UpdateStatusAndFailedLoginCountAsync).
///
public class ClientLoginLockoutTests
{
private const int ClientId = 42;
private const string Email = "user@example.com";
private const string CorrectPassword = "Correct-Horse-Battery-Staple";
private static (ClientAuthBLL Svc, Mock ClientUserDal) BuildService(int failedLoginThreshold = 5)
{
var clientUserDal = new Mock();
var clientUserRoleDal = new Mock();
clientUserRoleDal
.Setup(d => d.GetActiveRoleCodesAsync(It.IsAny(), It.IsAny(), It.IsAny()))
.ReturnsAsync(Enumerable.Empty());
var refreshTokenDal = new Mock();
var jwtService = new Mock();
jwtService.Setup(j => j.HashRefreshToken(It.IsAny())).Returns((string raw) => $"HASH-{raw}");
jwtService.Setup(j => j.IssueTokenPairAsync(It.IsAny(), It.IsAny()))
.ReturnsAsync(new ClientTokenPair
{
AccessToken = "access-token",
AccessTokenExpiresOn = DateTime.UtcNow.AddMinutes(15),
RefreshToken = "raw-refresh-token",
RefreshTokenHash = "HASH-raw-refresh-token",
RefreshTokenExpiresOn = DateTime.UtcNow.AddDays(30)
});
refreshTokenDal
.Setup(d => d.SaveAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(Task.CompletedTask);
clientUserDal
.Setup(d => d.UpdateFailedLoginCountAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(Task.CompletedTask);
clientUserDal
.Setup(d => d.UpdateLastLoginOnAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(Task.CompletedTask);
clientUserDal
.Setup(d => d.UpdateStatusAndFailedLoginCountAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(Task.CompletedTask);
clientUserDal
.Setup(d => d.UpdateStatusAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(Task.CompletedTask);
// Real AutoNumber wired to a mocked IQueryExecutor — only exercised on a successful login
// (IssueAndPersistTokenPairAsync's refresh-token id allocation).
var queryExecutor = new Mock();
queryExecutor
.Setup(q => q.QueryAsync(It.IsAny(), It.IsAny(), It.IsAny