using EntitlementBLL.Legal;
using EntitlementDAL.DTOs;
using EntitlementDAL.Interfaces;
using GB5Shared.DTO.Framework.AutoNumber;
using GB5Shared.DTO.Framework.CommonConfig;
using GB5Shared.DTO.Framework.Login;
using GB5Shared.GenerateAutoNumber;
using GB5Shared.QueryExecutor;
using Microsoft.Extensions.Options;
using Moq;
using System.Data.Common;
using Xunit;
namespace EntitlementTests;
///
/// Covers the P0 Legal/Contract Agreement Consent integration link (tracker §50/§51):
/// AgreementAuthoringBLL's Draft/Published/Superseded lifecycle guards, and
/// LocalAgreementConsentProvider's bundle resolution + append-only, re-validated acceptance
/// recording.
///
public class AgreementConsentTests
{
private static AutoNumber BuildAutoNumber(long nextId)
{
var queryExecutor = new Mock();
queryExecutor
.Setup(q => q.QueryAsync(
It.IsAny(), It.IsAny(), It.IsAny(),
It.IsAny(), It.IsAny(), It.IsAny()))
.ReturnsAsync(new List { nextId });
var dbConfig = new Mock>();
dbConfig.Setup(o => o.Value).Returns(new Gb5SystemDTO { DataBaseType = 0 });
return new AutoNumber(queryExecutor.Object, dbConfig.Object);
}
private static LoginDTO Login() => new() { ClientId = -1, UserId = -1 };
// ── AgreementAuthoringBLL ────────────────────────────────────────────────
[Fact]
public async Task EnsureTypeAsync_TypeAlreadyExists_ReturnsExisting_NoInsert()
{
var dal = new Mock();
var existing = new AgreementTypeDTO { AgreementTypeId = 1, TypeCode = "TOS", TypeName = "Terms of Service" };
dal.Setup(d => d.GetTypeByCodeAsync("TOS", It.IsAny(), It.IsAny())).ReturnsAsync(existing);
var bll = new AgreementAuthoringBLL(dal.Object, BuildAutoNumber(9001));
var result = await bll.EnsureTypeAsync("TOS", "Terms of Service", false, Login(), CancellationToken.None);
Assert.Same(existing, result);
dal.Verify(d => d.InsertTypeAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
}
[Fact]
public async Task EnsureTypeAsync_NotFound_CreatesNewType()
{
var dal = new Mock();
dal.Setup(d => d.GetTypeByCodeAsync("EULA", It.IsAny(), It.IsAny())).ReturnsAsync((AgreementTypeDTO?)null);
var bll = new AgreementAuthoringBLL(dal.Object, BuildAutoNumber(9001));
var result = await bll.EnsureTypeAsync("EULA", "End User License Agreement", true, Login(), CancellationToken.None);
Assert.Equal(9000, result.AgreementTypeId);
Assert.True(result.RequiresIndividualAcceptance);
dal.Verify(d => d.InsertTypeAsync(
It.Is(t => t.TypeCode == "EULA" && t.AgreementTypeId == 9000),
It.IsAny(), It.IsAny()), Times.Once);
}
[Theory]
[InlineData("", "Name")]
[InlineData("CODE", "")]
public async Task EnsureTypeAsync_MissingRequiredField_Throws(string code, string name)
{
var dal = new Mock();
var bll = new AgreementAuthoringBLL(dal.Object, BuildAutoNumber(1));
await Assert.ThrowsAsync(() => bll.EnsureTypeAsync(code, name, false, Login(), CancellationToken.None));
}
[Fact]
public async Task CreateDraftVersionAsync_TypeDoesNotExist_Throws()
{
var dal = new Mock();
dal.Setup(d => d.GetTypeByCodeAsync("TOS", It.IsAny(), It.IsAny())).ReturnsAsync((AgreementTypeDTO?)null);
var bll = new AgreementAuthoringBLL(dal.Object, BuildAutoNumber(1));
await Assert.ThrowsAsync(() =>
bll.CreateDraftVersionAsync("TOS", "v1.0", null, "https://example.com/tos-v1", DateTime.UtcNow, Login(), CancellationToken.None));
}
[Fact]
public async Task CreateDraftVersionAsync_HappyPath_CreatesDraft()
{
var dal = new Mock();
dal.Setup(d => d.GetTypeByCodeAsync("TOS", It.IsAny(), It.IsAny()))
.ReturnsAsync(new AgreementTypeDTO { AgreementTypeId = 5, TypeCode = "TOS" });
var bll = new AgreementAuthoringBLL(dal.Object, BuildAutoNumber(9002));
var result = await bll.CreateDraftVersionAsync("TOS", "v1.0", "IN", "https://example.com/tos-v1", DateTime.UtcNow, Login(), CancellationToken.None);
Assert.Equal(9001, result.AgreementVersionId);
Assert.Equal((byte)AgreementVersionStatus.Draft, result.VersionStatus);
dal.Verify(d => d.InsertVersionAsync(
It.Is(v => v.AgreementTypeId == 5 && v.JurisdictionCode == "IN" && v.VersionStatus == (byte)AgreementVersionStatus.Draft),
It.IsAny(), It.IsAny()), Times.Once);
}
[Fact]
public async Task PublishVersionAsync_NotFound_Throws()
{
var dal = new Mock();
dal.Setup(d => d.GetVersionByIdAsync(999, It.IsAny(), It.IsAny())).ReturnsAsync((AgreementVersionDTO?)null);
var bll = new AgreementAuthoringBLL(dal.Object, BuildAutoNumber(1));
await Assert.ThrowsAsync(() => bll.PublishVersionAsync(999, Login(), CancellationToken.None));
}
[Fact]
public async Task PublishVersionAsync_NotDraft_Throws()
{
var dal = new Mock();
dal.Setup(d => d.GetVersionByIdAsync(1, It.IsAny(), It.IsAny()))
.ReturnsAsync(new AgreementVersionDTO { AgreementVersionId = 1, VersionStatus = (byte)AgreementVersionStatus.Published });
var bll = new AgreementAuthoringBLL(dal.Object, BuildAutoNumber(1));
await Assert.ThrowsAsync(() => bll.PublishVersionAsync(1, Login(), CancellationToken.None));
dal.Verify(d => d.PublishVersionAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
}
[Fact]
public async Task PublishVersionAsync_Draft_PublishesWithComputedHash()
{
var dal = new Mock();
dal.Setup(d => d.GetVersionByIdAsync(1, It.IsAny(), It.IsAny()))
.ReturnsAsync(new AgreementVersionDTO { AgreementVersionId = 1, VersionStatus = (byte)AgreementVersionStatus.Draft, ContentRef = "https://example.com/tos-v1" });
var bll = new AgreementAuthoringBLL(dal.Object, BuildAutoNumber(1));
await bll.PublishVersionAsync(1, Login(), CancellationToken.None);
dal.Verify(d => d.PublishVersionAsync(1, It.Is(h => !string.IsNullOrEmpty(h)), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once);
}
[Fact]
public async Task SupersedeVersionAsync_NotPublished_Throws()
{
var dal = new Mock();
dal.Setup(d => d.GetVersionByIdAsync(1, It.IsAny(), It.IsAny()))
.ReturnsAsync(new AgreementVersionDTO { AgreementVersionId = 1, VersionStatus = (byte)AgreementVersionStatus.Draft });
var bll = new AgreementAuthoringBLL(dal.Object, BuildAutoNumber(1));
await Assert.ThrowsAsync(() => bll.SupersedeVersionAsync(1, Login(), CancellationToken.None));
}
[Fact]
public async Task SupersedeVersionAsync_Published_Supersedes()
{
var dal = new Mock();
dal.Setup(d => d.GetVersionByIdAsync(1, It.IsAny(), It.IsAny()))
.ReturnsAsync(new AgreementVersionDTO { AgreementVersionId = 1, VersionStatus = (byte)AgreementVersionStatus.Published });
var bll = new AgreementAuthoringBLL(dal.Object, BuildAutoNumber(1));
await bll.SupersedeVersionAsync(1, Login(), CancellationToken.None);
dal.Verify(d => d.UpdateVersionStatusAsync(1, (byte)AgreementVersionStatus.Superseded, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once);
}
// ── LocalAgreementConsentProvider ────────────────────────────────────────
[Fact]
public async Task GetApplicableAgreementsAsync_NoTypeCodes_Throws()
{
var dal = new Mock();
var provider = new LocalAgreementConsentProvider(dal.Object, BuildAutoNumber(1));
await Assert.ThrowsAsync(() =>
provider.GetApplicableAgreementsAsync("IN", Array.Empty(), Login(), CancellationToken.None));
}
[Fact]
public async Task GetApplicableAgreementsAsync_HappyPath_ReturnsBundle()
{
var dal = new Mock();
var versions = new List { new() { AgreementVersionId = 1 }, new() { AgreementVersionId = 2 } };
dal.Setup(d => d.GetApplicableVersionsAsync(
It.Is>(c => c.Contains("TOS") && c.Contains("AUP")), "IN",
It.IsAny(), It.IsAny()))
.ReturnsAsync(versions);
var provider = new LocalAgreementConsentProvider(dal.Object, BuildAutoNumber(1));
var bundle = await provider.GetApplicableAgreementsAsync("IN", new[] { "TOS", "AUP" }, Login(), CancellationToken.None);
Assert.Equal(2, bundle.Versions.Count);
}
[Fact]
public async Task RecordAcceptanceAsync_NoVersionIds_Throws()
{
var dal = new Mock();
var provider = new LocalAgreementConsentProvider(dal.Object, BuildAutoNumber(1));
await Assert.ThrowsAsync(() =>
provider.RecordAcceptanceAsync(AgreementSubjectType.Prospect, 1, Array.Empty(), "1.2.3.4", "UA",
AgreementAcceptanceMethod.Checkbox, "corr-1", Login(), CancellationToken.None));
}
[Fact]
public async Task RecordAcceptanceAsync_EmptyCorrelationKey_Throws()
{
var dal = new Mock();
var provider = new LocalAgreementConsentProvider(dal.Object, BuildAutoNumber(1));
await Assert.ThrowsAsync(() =>
provider.RecordAcceptanceAsync(AgreementSubjectType.Prospect, 1, new[] { 10 }, "1.2.3.4", "UA",
AgreementAcceptanceMethod.Checkbox, "", Login(), CancellationToken.None));
}
[Fact]
public async Task RecordAcceptanceAsync_VersionNoLongerPublished_ThrowsAndInsertsNothing()
{
var dal = new Mock();
dal.Setup(d => d.GetVersionByIdAsync(10, It.IsAny(), It.IsAny()))
.ReturnsAsync(new AgreementVersionDTO { AgreementVersionId = 10, VersionStatus = (byte)AgreementVersionStatus.Superseded });
var provider = new LocalAgreementConsentProvider(dal.Object, BuildAutoNumber(1));
await Assert.ThrowsAsync(() =>
provider.RecordAcceptanceAsync(AgreementSubjectType.Prospect, 1, new[] { 10 }, "1.2.3.4", "UA",
AgreementAcceptanceMethod.Checkbox, "corr-1", Login(), CancellationToken.None));
dal.Verify(d => d.InsertAcceptanceAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
}
[Fact]
public async Task RecordAcceptanceAsync_HappyPath_InsertsOneRowPerVersion_SameCorrelationKey()
{
var dal = new Mock();
dal.Setup(d => d.GetVersionByIdAsync(It.IsAny(), It.IsAny(), It.IsAny()))
.ReturnsAsync((int id, LoginDTO _, CancellationToken _) => new AgreementVersionDTO { AgreementVersionId = id, VersionStatus = (byte)AgreementVersionStatus.Published });
var inserted = new List();
dal.Setup(d => d.InsertAcceptanceAsync(It.IsAny(), It.IsAny(), It.IsAny()))
.Callback((dto, _, _) => inserted.Add(dto))
.Returns(Task.CompletedTask);
var provider = new LocalAgreementConsentProvider(dal.Object, BuildAutoNumber(9003));
await provider.RecordAcceptanceAsync(AgreementSubjectType.Prospect, 42, new[] { 10, 11 }, "1.2.3.4", "UA",
AgreementAcceptanceMethod.Clickwrap, "corr-1", Login(), CancellationToken.None);
Assert.Equal(2, inserted.Count);
Assert.All(inserted, a => Assert.Equal("corr-1", a.CorrelationKey));
Assert.All(inserted, a => Assert.Equal(42, a.SubjectId));
Assert.All(inserted, a => Assert.Equal((byte)AgreementSubjectType.Prospect, a.SubjectType));
Assert.All(inserted, a => Assert.Equal((byte)AgreementAcceptanceMethod.Clickwrap, a.AcceptanceMethod));
Assert.Contains(inserted, a => a.AgreementVersionId == 10);
Assert.Contains(inserted, a => a.AgreementVersionId == 11);
}
}