using DocumentService.Application.Common.DTOs;
using DocumentService.Client.Clients;
using DocumentService.Client.Interfaces;
using FluentAssertions;
using Moq;
using System.Net.Http;
using System.Reflection;
namespace DocumentService.Tests.Unit.Client;
///
/// Unit tests for .
/// Both and are mocked –
/// no real HTTP calls are made. Tests cover every overload, every branch, and all edge cases.
///
public class WorkflowsClientTests
{
// ─── test doubles ────────────────────────────────────────────────────────
private readonly Mock _validation = new();
private readonly Mock _swissQrCode = new();
private WorkflowsClient Sut => new(_validation.Object, _swissQrCode.Object);
// Extension methods are defined on IWorkflowsClient — use this for filePath overload tests.
private IWorkflowsClient SutAsInterface => Sut;
// ─── fixture data ─────────────────────────────────────────────────────────
private static byte[] ValidPdfBytes() => LoadTestPdf("valid.pdf");
private static byte[] EncryptedPdfBytes() => LoadTestPdf("encryptedPdf.pdf");
private static byte[] QrPdfBytes() => LoadTestPdf("pdfWithSwissQRCode.pdf");
private static byte[] LoadTestPdf(string filename)
{
var assembly = Assembly.GetExecutingAssembly();
var resourceName = $"DocumentService.Tests.TestData.Pdfs.{filename}";
using var stream = assembly.GetManifestResourceStream(resourceName)
?? throw new FileNotFoundException(
$"Embedded resource '{resourceName}' not found. " +
$"Available: {string.Join(", ", assembly.GetManifestResourceNames())}");
using var ms = new MemoryStream();
stream.CopyTo(ms);
return ms.ToArray();
}
// ─── valid PdfValidationResult helpers ───────────────────────────────────
private static PdfValidationResult ValidResult() =>
new(PageCount: 1, FileSizeBytes: 1024, FileSizeMB: 0.001,
PdfVersion: "1.4", HasAttachments: false, AttachmentCount: 0,
IsEncrypted: false);
private static PdfValidationResult InvalidResult_Encrypted() =>
new(PageCount: 1, FileSizeBytes: 1024, FileSizeMB: 0.001,
PdfVersion: "1.4", HasAttachments: false, AttachmentCount: 0,
IsEncrypted: true);
private static PdfValidationResult InvalidResult_EmptyFile() =>
new(PageCount: 0, FileSizeBytes: 0, FileSizeMB: 0,
PdfVersion: "", HasAttachments: false, AttachmentCount: 0,
IsEncrypted: false);
private static SwissQrCodeExtractionResult QrResult() =>
new(
Bill: new SwissQrBillDto
{
Version = "V2_0",
Currency = "CHF",
Amount = 630.20m,
Account = "CH9300762011623852957",
Creditor = new AddressDto { Name = "ALMAT AG", Town = "Tagelswangen", CountryCode = "CH", Type = "S", PostalCode = "8317" },
ReferenceType = "QRR",
Reference = "000000000000000000252080824"
},
RawLines: ["SPC", "0200", "1"]
);
private static SwissQrCodeExtractionResult RawQrResult() =>
new(Bill: (SwissQrBillDto?)null, RawLines: ["SPC", "0200", "1", "CH9300762011623852957"]);
[Fact]
public async Task Inspect_Bytes_ValidPdf_ReturnsBothValidationAndQrCode()
{
// Arrange
var pdfBytes = ValidPdfBytes();
_validation.Setup(v => v.ValidateAsync(pdfBytes, It.IsAny()))
.ReturnsAsync(ValidResult());
_swissQrCode.Setup(q => q.ExtractAsync(pdfBytes, false, It.IsAny()))
.ReturnsAsync(QrResult());
// Act
var result = await Sut.InspectSwissQrCodeAsync(pdfBytes);
// Assert
result.Validation.IsValid.Should().BeTrue();
result.QrCode.Should().NotBeNull();
result.QrCode!.Bill!.Currency.Should().Be("CHF");
_validation.Verify(v => v.ValidateAsync(pdfBytes, It.IsAny()), Times.Once);
_swissQrCode.Verify(q => q.ExtractAsync(pdfBytes, false, It.IsAny()), Times.Once);
}
[Fact]
public async Task Inspect_Bytes_InvalidPdf_ReturnsValidationWithNullQrCode_NeverThrows()
{
// Arrange
var pdfBytes = EncryptedPdfBytes();
_validation.Setup(v => v.ValidateAsync(pdfBytes, It.IsAny()))
.ReturnsAsync(InvalidResult_Encrypted());
// Act
var result = await Sut.InspectSwissQrCodeAsync(pdfBytes);
// Assert – no exception, validation details always available
result.Validation.IsValid.Should().BeFalse();
result.Validation.IsEncrypted.Should().BeTrue();
result.QrCode.Should().BeNull("extraction must not run on an invalid document");
_swissQrCode.Verify(q => q.ExtractAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
}
[Fact]
public async Task Inspect_Bytes_ValidationAlwaysPopulated_RegardlessOfQrOutcome()
{
// Arrange – valid PDF but QR extraction returns empty lines (no QR code found)
var pdfBytes = ValidPdfBytes();
var emptyQr = new SwissQrCodeExtractionResult(Bill: (SwissQrBillDto?)null, RawLines: []);
_validation.Setup(v => v.ValidateAsync(pdfBytes, It.IsAny()))
.ReturnsAsync(ValidResult());
_swissQrCode.Setup(q => q.ExtractAsync(pdfBytes, false, It.IsAny()))
.ReturnsAsync(emptyQr);
// Act
var result = await Sut.InspectSwissQrCodeAsync(pdfBytes);
// Assert – validation is always there even when QR is empty
result.Validation.Should().NotBeNull();
result.Validation.PageCount.Should().Be(1);
result.QrCode.Should().NotBeNull(); // extraction was attempted
}
[Fact]
public async Task Inspect_Bytes_RawMode_PassesRawFlagToExtraction()
{
// Arrange
var pdfBytes = QrPdfBytes();
_validation.Setup(v => v.ValidateAsync(pdfBytes, It.IsAny()))
.ReturnsAsync(ValidResult());
_swissQrCode.Setup(q => q.ExtractAsync(pdfBytes, true, It.IsAny()))
.ReturnsAsync(RawQrResult());
// Act
var result = await Sut.InspectSwissQrCodeAsync(pdfBytes, raw: true);
// Assert
result.QrCode!.RawLines.Should().Contain("SPC");
_swissQrCode.Verify(q => q.ExtractAsync(pdfBytes, true, It.IsAny()), Times.Once);
}
[Fact]
public async Task Inspect_Bytes_CancellationToken_ForwardedToBothClients()
{
// Arrange
var pdfBytes = ValidPdfBytes();
var cts = new CancellationTokenSource();
var token = cts.Token;
_validation.Setup(v => v.ValidateAsync(pdfBytes, token)).ReturnsAsync(ValidResult());
_swissQrCode.Setup(q => q.ExtractAsync(pdfBytes, false, token)).ReturnsAsync(QrResult());
// Act
await Sut.InspectSwissQrCodeAsync(pdfBytes, cancellationToken: token);
// Assert
_validation.Verify(v => v.ValidateAsync(pdfBytes, token), Times.Once);
_swissQrCode.Verify(q => q.ExtractAsync(pdfBytes, false, token), Times.Once);
}
[Fact]
public async Task Inspect_Stream_ValidPdf_ReturnsBothResults()
{
// Arrange
_validation.Setup(v => v.ValidateAsync(It.IsAny(), It.IsAny()))
.ReturnsAsync(ValidResult());
_swissQrCode.Setup(q => q.ExtractAsync(It.IsAny(), false, It.IsAny()))
.ReturnsAsync(QrResult());
using var pdfStream = new MemoryStream(ValidPdfBytes());
// Act
var result = await Sut.InspectSwissQrCodeAsync(pdfStream);
// Assert
result.Validation.IsValid.Should().BeTrue();
result.QrCode.Should().NotBeNull();
}
[Fact]
public async Task Inspect_Stream_InvalidPdf_ReturnsValidationWithNullQrCode_NeverThrows()
{
// Arrange
_validation.Setup(v => v.ValidateAsync(It.IsAny(), It.IsAny()))
.ReturnsAsync(InvalidResult_Encrypted());
using var pdfStream = new MemoryStream(EncryptedPdfBytes());
// Act
var result = await Sut.InspectSwissQrCodeAsync(pdfStream);
// Assert
result.Validation.IsValid.Should().BeFalse();
result.QrCode.Should().BeNull();
_swissQrCode.Verify(q => q.ExtractAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
}
[Fact]
public async Task Inspect_FilePath_ValidPdf_ReturnsBothResults()
{
// Arrange – extension opens a FileStream so mock expects Stream overload
var tempPath = Path.GetTempFileName();
try
{
await File.WriteAllBytesAsync(tempPath, ValidPdfBytes());
_validation.Setup(v => v.ValidateAsync(It.IsAny(), It.IsAny()))
.ReturnsAsync(ValidResult());
_swissQrCode.Setup(q => q.ExtractAsync(It.IsAny(), false, It.IsAny()))
.ReturnsAsync(QrResult());
// Act – calls extension method which opens a FileStream and delegates to Stream overload
var result = await SutAsInterface.InspectSwissQrCodeAsync(tempPath);
// Assert
result.Validation.IsValid.Should().BeTrue();
result.QrCode.Should().NotBeNull();
}
finally { File.Delete(tempPath); }
}
[Fact]
public async Task Inspect_FilePath_InvalidPdf_ReturnsValidationWithNullQrCode_NeverThrows()
{
// Arrange
var tempPath = Path.GetTempFileName();
try
{
await File.WriteAllBytesAsync(tempPath, EncryptedPdfBytes());
_validation.Setup(v => v.ValidateAsync(It.IsAny(), It.IsAny()))
.ReturnsAsync(InvalidResult_Encrypted());
// Act
var result = await SutAsInterface.InspectSwissQrCodeAsync(tempPath);
// Assert – never throws, validation details always populated
result.Validation.IsValid.Should().BeFalse();
result.QrCode.Should().BeNull();
}
finally { File.Delete(tempPath); }
}
}