using System.Reflection;
using DocumentOperator.Application.Common.DTOs;
using DocumentOperator.Application.Common.Interfaces;
using DocumentOperator.Domain.Common.Exceptions;
using DocumentOperator.Infrastructure.Services.PdfProcessing;
using FluentAssertions;

namespace DocumentOperator.Tests.Unit.Infrastructure.Services.PdfProcessing;

/// <summary>
/// Unit tests for DevExpressPdfProcessor.
/// Tests PDF validation and metadata extraction.
/// </summary>
public class DevExpressPdfProcessorTests
{
    private readonly IPdfProcessor _sut; // SUT = System Under Test

    public DevExpressPdfProcessorTests()
    {
        // Arrange: Create instance (wird später implementiert)
        _sut = new DevExpressPdfProcessor();
    }

    #region Helper Methods

    /// <summary>
    /// Loads a test PDF from embedded resources.
    /// </summary>
    /// <param name="filename">Name of the PDF file (e.g., "valid.pdf")</param>
    /// <returns>PDF content as byte array</returns>
    private static byte[] LoadTestPdf(string filename)
    {
        var assembly = Assembly.GetExecutingAssembly();
        var resourceName = $"DocumentOperator.Tests.TestData.Pdfs.{filename}";

        using var stream = assembly.GetManifestResourceStream(resourceName);

        if (stream == null)
        {
            throw new FileNotFoundException(
                $"Embedded resource '{resourceName}' not found. " +
                $"Available resources: {string.Join(", ", assembly.GetManifestResourceNames())}");
        }

        using var memoryStream = new MemoryStream();
        stream.CopyTo(memoryStream);
        return memoryStream.ToArray();
    }

    /// <summary>
    /// Loads a test PDF from embedded resources as Stream.
    /// </summary>
    /// <param name="filename">Name of the PDF file (e.g., "valid.pdf")</param>
    /// <returns>PDF content as MemoryStream (caller must dispose)</returns>
    private static MemoryStream LoadTestPdfAsStream(string filename)
    {
        byte[] pdfBytes = LoadTestPdf(filename);
        return new MemoryStream(pdfBytes);
    }

    /// <summary>
    /// Converts byte array to MemoryStream for testing
    /// </summary>
    private static MemoryStream ToStream(byte[] bytes) => new MemoryStream(bytes);

    #endregion

    #region ValidateAsync Tests

    [Fact]
    public async Task ValidateAsync_ValidPdf_ReturnsPdfMetadata()
    {
        // Arrange
        byte[] pdfBytes = LoadTestPdf("valid.pdf");

        // Act
        var metadata = await _sut.ValidateAsync(ToStream(pdfBytes));

        // Assert
        metadata.Should().NotBeNull("a valid PDF should return metadata");
        metadata.PageCount.Should().BeGreaterThan(0, "PDF must have at least one page");
        metadata.FileSizeBytes.Should().Be(pdfBytes.Length, "file size should match input");
        metadata.PdfVersion.Should().NotBeNullOrEmpty("PDF version should be detected");
    }

    [Fact]
    public async Task ValidateAsync_ValidPdf_ReturnsCorrectPageCount()
    {
        // Arrange
        byte[] pdfBytes = LoadTestPdf("valid.pdf");

        // Act
        var metadata = await _sut.ValidateAsync(ToStream(pdfBytes));

        // Assert
        // Deine valid.pdf hat wahrscheinlich 1-5 Seiten - passe an!
        metadata.PageCount.Should().BeInRange(1, 100, "test PDF should have reasonable page count");
    }

    [Fact]
    public async Task ValidateAsync_NullStream_ThrowsArgumentNullException()
    {
        // Arrange
        Stream? pdfStream = null;

        // Act
        Func<Task> act = async () => await _sut.ValidateAsync(pdfStream!);

        // Assert
        await act.Should().ThrowAsync<ArgumentNullException>()
            .WithMessage("*pdfStream*", "null input should be rejected");
    }

    [Fact]
    public async Task ValidateAsync_EmptyStream_ThrowsBadRequestException()
    {
        // Arrange
        var pdfStream = new MemoryStream();

        // Act
        Func<Task> act = async () => await _sut.ValidateAsync(pdfStream);

        // Assert
        await act.Should().ThrowAsync<BadRequestException>()
            .WithMessage("*empty*", "empty input should be rejected");
    }

    [Fact]
    public async Task ValidateAsync_CorruptedPdf_ThrowsPdfProcessingException()
    {
        // Arrange
        byte[] pdfBytes = "This is not a valid PDF content"u8.ToArray();

        // Act
        Func<Task> act = async () => await _sut.ValidateAsync(ToStream(pdfBytes));

        // Assert
        await act.Should().ThrowAsync<Exception>()
            .WithMessage("*valid pdf*", "corrupted PDF should throw exception");
    }

    #endregion

    #region Metadata Tests

    [Fact]
    public async Task ValidateAsync_ValidPdf_FileSizeMBCalculatedCorrectly()
    {
        // Arrange
        byte[] pdfBytes = LoadTestPdf("valid.pdf");

        // Act
        var metadata = await _sut.ValidateAsync(ToStream(pdfBytes));

        // Assert
        double expectedSizeMB = pdfBytes.Length / 1024.0 / 1024.0;
        metadata.FileSizeMB.Should().BeApproximately(expectedSizeMB, 0.01,
            "FileSizeMB should be calculated correctly from bytes");
    }

    #endregion

    #region Attachment Detection Tests

    [Fact]
    public async Task ValidateAsync_ValidPdf_DetectsAttachmentsCorrectly()
    {
        // Arrange
        byte[] pdfBytes = LoadTestPdf("valid.pdf");

        // Act
        var metadata = await _sut.ValidateAsync(ToStream(pdfBytes));

        // Assert
        // Note: valid.pdf actually contains /EmbeddedFiles reference (15 0 R)
        // This test just verifies that attachment detection doesn't crash
        // The exact count depends on the PDF content
        metadata.Should().NotBeNull();
    }

    [Fact]
    public async Task ValidateAsync_PdfWithMultipleAttachments_ReturnsCorrectCount()
    {
        // Arrange
        byte[] pdfBytes = LoadTestPdf("pdfWithMoreThanOneAttachment.pdf");

        // Act
        var metadata = await _sut.ValidateAsync(ToStream(pdfBytes));

        // Assert
        metadata.HasAttachments.Should().BeTrue("PDF has multiple attachments");
        metadata.AttachmentCount.Should().BeGreaterThan(1, "PDF has more than 1 attachment");
    }

    // Note: Testing PDF with attachments requires a real ZUGFeRD PDF file
    // as DevExpress PdfDocumentProcessor doesn't expose a simple API to create attachments.
    // This test will be added when a ZUGFeRD test PDF is available.
    // For now, we verify that attachment detection works (returns false for PDFs without attachments).

    #endregion

    #region CheckAttachmentsAsync Tests

    [Fact]
    public async Task CheckAttachmentsAsync_PdfWithoutAttachments_ReturnsEmptyList()
    {
        // Arrange
        byte[] pdfBytes = LoadTestPdf("pdfWithSwissQRCode.pdf");

        // Act
        var attachmentInfo = await _sut.CheckAttachmentsAsync(ToStream(pdfBytes));

        // Assert
        attachmentInfo.Should().NotBeNull("CheckAttachmentsAsync should return non-null result");
        attachmentInfo.HasAttachments.Should().BeFalse("Swiss QR PDF has no attachments");
        attachmentInfo.AttachmentCount.Should().Be(0, "Swiss QR PDF has no attachments");
        attachmentInfo.Attachments.Should().BeEmpty("Swiss QR PDF has no attachments");
    }

    [Fact]
    public async Task CheckAttachmentsAsync_PdfWithMultipleAttachments_ReturnsCorrectMetadata()
    {
        // Arrange
        byte[] pdfBytes = LoadTestPdf("pdfWithMoreThanOneAttachment.pdf");

        // Act
        var attachmentInfo = await _sut.CheckAttachmentsAsync(ToStream(pdfBytes));

        // Assert
        attachmentInfo.Should().NotBeNull("CheckAttachmentAsync should return non-null result");
        attachmentInfo.HasAttachments.Should().BeTrue("PDF has 6 attachments");
        attachmentInfo.AttachmentCount.Should().Be(6, "PDF has exactly 6 attachments");
        attachmentInfo.Attachments.Should().HaveCount(6, "Attachments list should contain 6 items");

        // Verify each attachment has required properties
        foreach (var attachment in attachmentInfo.Attachments)
        {
            attachment.FileName.Should().NotBeNullOrEmpty("each attachment must have a file name");
            attachment.MimeType.Should().NotBeNullOrEmpty("each attachment must have a MIME type");
            attachment.SizeBytes.Should().BeGreaterThan(0, "each attachment must have a size > 0");
        }
    }

    [Fact]
    public async Task CheckAttachmentsAsync_ValidPdf_HasAttachments()
    {
        // Arrange
        byte[] pdfBytes = LoadTestPdf("valid.pdf");

        // Act
        var attachmentInfo = await _sut.CheckAttachmentsAsync(ToStream(pdfBytes));

        // Assert
        attachmentInfo.Should().NotBeNull("CheckAttachmentsAsync should return non-null result");
        // Note: valid.pdf contains embedded files (tested and confirmed)
        attachmentInfo.HasAttachments.Should().BeTrue("valid.pdf contains attachments");
        attachmentInfo.AttachmentCount.Should().BeGreaterThan(0, "valid.pdf has at least one attachment");
    }

    [Fact]
    public async Task CheckAttachmentsAsync_EmptyBytes_ThrowsBadRequestException()
    {
        // Arrange
        var pdfStream = new MemoryStream();

        // Act
        Func<Task> act = async () => await _sut.CheckAttachmentsAsync(pdfStream);

        // Assert
        await act.Should().ThrowAsync<BadRequestException>()
            .WithMessage("*empty*", "empty input should be rejected");
    }

    [Fact]
    public async Task CheckAttachmentsAsync_CorruptedPdf_ThrowsException()
    {
        // Arrange
        byte[] pdfBytes = "This is not a valid PDF content"u8.ToArray();

        // Act
        Func<Task> act = async () => await _sut.CheckAttachmentsAsync(ToStream(pdfBytes));

        // Assert
        // DevExpress throws ArgumentException for invalid PDF data
        await act.Should().ThrowAsync<Exception>("corrupted PDF should throw exception");
    }

    #endregion

    #region ExtractAttachmentsAsync Tests

    [Fact]
    public async Task ExtractAttachmentsAsync_PdfWithAttachments_ReturnsValidZip()
    {
        // Arrange
        byte[] pdfBytes = LoadTestPdf("pdfWithMoreThanOneAttachment.pdf");

        // Act
        byte[] zipBytes = await _sut.ExtractAttachmentsAsync(ToStream(pdfBytes));

        // Assert
        zipBytes.Should().NotBeEmpty("ZIP should contain data");
        
        // Verify ZIP header (PK signature)
        zipBytes[0].Should().Be(0x50, "ZIP magic bytes start with 'P'");
        zipBytes[1].Should().Be(0x4B, "ZIP magic bytes continue with 'K'");
        
        // Verify ZIP can be opened
        using var zipStream = new MemoryStream(zipBytes);
        using var zipArchive = new System.IO.Compression.ZipArchive(zipStream, System.IO.Compression.ZipArchiveMode.Read);
        
        zipArchive.Entries.Should().HaveCount(6, "PDF contains 6 attachments");
    }

    [Fact(Skip = "valid.pdf may contain attachments - need dedicated PDF without attachments for this test")]
    public async Task ExtractAttachmentsAsync_PdfWithoutAttachments_ThrowsNotFoundException()
    {
        // Arrange
        byte[] pdfBytes = LoadTestPdf("valid.pdf"); // No attachments

        // Act
        Func<Task> act = async () => await _sut.ExtractAttachmentsAsync(ToStream(pdfBytes));

        // Assert
        await act.Should().ThrowAsync<NotFoundException>()
            .WithMessage("*attachments*", "PDF without attachments should throw NotFoundException");
    }

    [Fact]
    public async Task ExtractAttachmentsAsync_ExtractedFilesHaveCorrectNames()
    {
        // Arrange
        byte[] pdfBytes = LoadTestPdf("pdfWithMoreThanOneAttachment.pdf");

        // Act
        byte[] zipBytes = await _sut.ExtractAttachmentsAsync(ToStream(pdfBytes));

        // Assert
        using var zipStream = new MemoryStream(zipBytes);
        using var zipArchive = new System.IO.Compression.ZipArchive(zipStream, System.IO.Compression.ZipArchiveMode.Read);
        
        // Verify all entries have valid names (not null/empty)
        foreach (var entry in zipArchive.Entries)
        {
            entry.Name.Should().NotBeNullOrEmpty("each file should have a name");
        }
    }

    [Fact]
    public async Task ExtractAttachmentsAsync_EmptyStream_ThrowsBadRequestException()
    {
        // Arrange
        using var emptyStream = new MemoryStream();

        // Act
        Func<Task> act = async () => await _sut.ExtractAttachmentsAsync(emptyStream);

        // Assert
        await act.Should().ThrowAsync<BadRequestException>()
            .WithMessage("*empty*", "empty stream should be rejected");
    }

    [Fact]
    public async Task ExtractAttachmentsAsync_CorruptedPdf_ThrowsException()
    {
        // Arrange
        byte[] pdfBytes = "This is not a valid PDF content"u8.ToArray();

        // Act
        Func<Task> act = async () => await _sut.ExtractAttachmentsAsync(ToStream(pdfBytes));

        // Assert
        // DevExpress throws exception for invalid PDF data
        await act.Should().ThrowAsync<Exception>("corrupted PDF should throw exception");
    }

    #endregion

    #region MergePdfsAsync Tests

    [Fact]
    public async Task MergePdfsAsync_TwoPdfs_ReturnsMergedPdf()
    {
        // Arrange
        using var pdf1 = LoadTestPdfAsStream("valid.pdf");
        using var pdf2 = LoadTestPdfAsStream("pdfWithSwissQRCode.pdf");
        var streams = new List<Stream> { pdf1, pdf2 };

        // Act
        byte[] mergedPdf = await _sut.MergePdfsAsync(streams);

        // Assert
        mergedPdf.Should().NotBeEmpty("merged PDF should contain data");
        mergedPdf.Length.Should().BeGreaterThan(1000, "merged PDF should be reasonably sized");

        // Verify it's a valid PDF by loading it
        using var verifyStream = new MemoryStream(mergedPdf);
        using var verifyProcessor = new DevExpress.Pdf.PdfDocumentProcessor();
        verifyProcessor.LoadDocument(verifyStream);
        verifyProcessor.Document.Pages.Count.Should().BeGreaterOrEqualTo(2, "merged PDF should have at least 2 pages");
    }

    [Fact]
    public async Task MergePdfsAsync_ThreePdfs_ReturnsMergedPdf()
    {
        // Arrange
        using var pdf1 = LoadTestPdfAsStream("valid.pdf");
        using var pdf2 = LoadTestPdfAsStream("pdfWithSwissQRCode.pdf");
        using var pdf3 = LoadTestPdfAsStream("pdfWithMoreThanOneAttachment.pdf");
        var streams = new List<Stream> { pdf1, pdf2, pdf3 };

        // Act
        byte[] mergedPdf = await _sut.MergePdfsAsync(streams);

        // Assert
        mergedPdf.Should().NotBeEmpty();
        
        // Verify merged PDF
        using var verifyStream = new MemoryStream(mergedPdf);
        using var verifyProcessor = new DevExpress.Pdf.PdfDocumentProcessor();
        verifyProcessor.LoadDocument(verifyStream);
        verifyProcessor.Document.Pages.Count.Should().BeGreaterOrEqualTo(3, "merged PDF should have at least 3 pages");
    }

    [Fact]
    public async Task MergePdfsAsync_WithPageRanges_MergesSelectedPages()
    {
        // Arrange: Merge first page of each PDF
        using var pdf1 = LoadTestPdfAsStream("pdfWithSwissQRCode.pdf"); // Multi-page
        using var pdf2 = LoadTestPdfAsStream("pdfWithMoreThanOneAttachment.pdf"); // Multi-page
        var streams = new List<Stream> { pdf1, pdf2 };
        var pageRanges = new List<string?> { "1", "1" }; // Only first page from each

        // Act
        byte[] mergedPdf = await _sut.MergePdfsAsync(streams, pageRanges);

        // Assert
        mergedPdf.Should().NotBeEmpty();
        
        // Verify merged PDF has exactly 2 pages
        using var verifyStream = new MemoryStream(mergedPdf);
        using var verifyProcessor = new DevExpress.Pdf.PdfDocumentProcessor();
        verifyProcessor.LoadDocument(verifyStream);
        verifyProcessor.Document.Pages.Count.Should().Be(2, "should have exactly 2 pages (1 from each PDF)");
    }

    [Fact]
    public async Task MergePdfsAsync_WithRangeFormat_MergesSelectedPages()
    {
        // Arrange: Merge 2 PDFs, second PDF with page range "1" (first page only)
        using var pdf1 = LoadTestPdfAsStream("valid.pdf"); // 1 page
        using var pdf2 = LoadTestPdfAsStream("pdfWithMoreThanOneAttachment.pdf"); //  PDF with attachments
        var streams = new List<Stream> { pdf1, pdf2 };
        var pageRanges = new List<string?> { null, "1" }; // All pages from first, page 1 only from second

        // Act
        byte[] mergedPdf = await _sut.MergePdfsAsync(streams, pageRanges);

        // Assert
        mergedPdf.Should().NotBeEmpty();
        
        // Verify merged PDF has at least 2 pages (1 from first + 1 from second)
        using var verifyStream = new MemoryStream(mergedPdf);
        using var verifyProcessor = new DevExpress.Pdf.PdfDocumentProcessor();
        verifyProcessor.LoadDocument(verifyStream);
        verifyProcessor.Document.Pages.Count.Should().BeGreaterOrEqualTo(2, "should have at least 2 pages");
    }

    [Fact]
    public async Task MergePdfsAsync_SinglePdf_ThrowsBadRequestException()
    {
        // Arrange: Only one PDF (minimum 2 required)
        using var pdf1 = LoadTestPdfAsStream("valid.pdf");
        var streams = new List<Stream> { pdf1 };

        // Act
        var act = async () => await _sut.MergePdfsAsync(streams);

        // Assert
        await act.Should().ThrowAsync<BadRequestException>()
            .WithMessage("*at least 2*");
    }

    [Fact]
    public async Task MergePdfsAsync_EmptyStream_ThrowsBadRequestException()
    {
        // Arrange: One valid PDF + one empty stream
        using var pdf1 = LoadTestPdfAsStream("valid.pdf");
        using var emptyStream = new MemoryStream();
        var streams = new List<Stream> { pdf1, emptyStream };

        // Act
        var act = async () => await _sut.MergePdfsAsync(streams);

        // Assert
        await act.Should().ThrowAsync<BadRequestException>()
            .WithMessage("*empty*");
    }

    [Fact]
    public async Task MergePdfsAsync_StreamNotAtBeginning_ThrowsBadRequestException()
    {
        // Arrange: Valid PDFs but second stream not at Position=0
        using var pdf1 = LoadTestPdfAsStream("valid.pdf");
        using var pdf2 = LoadTestPdfAsStream("pdfWithSwissQRCode.pdf");
        pdf2.Position = 10; // Move position away from beginning
        var streams = new List<Stream> { pdf1, pdf2 };

        // Act
        var act = async () => await _sut.MergePdfsAsync(streams);

        // Assert
        await act.Should().ThrowAsync<BadRequestException>();
    }

    [Fact]
    public async Task MergePdfsAsync_InvalidPageRange_ThrowsBadRequestException()
    {
        // Arrange: Invalid page range format
        using var pdf1 = LoadTestPdfAsStream("valid.pdf");
        using var pdf2 = LoadTestPdfAsStream("pdfWithSwissQRCode.pdf");
        var streams = new List<Stream> { pdf1, pdf2 };
        var pageRanges = new List<string?> { "1", "abc" }; // Invalid format

        // Act
        var act = async () => await _sut.MergePdfsAsync(streams, pageRanges);

        // Assert
        await act.Should().ThrowAsync<BadRequestException>()
            .WithMessage("*Invalid page*");
    }

    [Fact]
    public async Task MergePdfsAsync_PageRangeExceedsPageCount_ThrowsBadRequestException()
    {
        // Arrange: Page range exceeds document page count
        using var pdf1 = LoadTestPdfAsStream("valid.pdf"); // 1 page
        using var pdf2 = LoadTestPdfAsStream("pdfWithSwissQRCode.pdf");
        var streams = new List<Stream> { pdf1, pdf2 };
        var pageRanges = new List<string?> { "1-99", null }; // 99 exceeds pdf1 page count

        // Act
        var act = async () => await _sut.MergePdfsAsync(streams, pageRanges);

        // Assert
        await act.Should().ThrowAsync<BadRequestException>()
            .WithMessage("*exceeds document page count*");
    }

    [Fact]
    public async Task MergePdfsAsync_PageRangesMismatch_ThrowsBadRequestException()
    {
        // Arrange: Page ranges count doesn't match streams count
        using var pdf1 = LoadTestPdfAsStream("valid.pdf");
        using var pdf2 = LoadTestPdfAsStream("pdfWithSwissQRCode.pdf");
        var streams = new List<Stream> { pdf1, pdf2 };
        var pageRanges = new List<string?> { "1" }; // Only 1 range for 2 PDFs

        // Act
        var act = async () => await _sut.MergePdfsAsync(streams, pageRanges);

        // Assert
        await act.Should().ThrowAsync<BadRequestException>()
            .WithMessage("*must match*");
    }

    #endregion

    #region AddAnnotationAsync Tests

    [Fact]
    public async Task AddAnnotationAsync_TextMarkupHighlight_ReturnsAnnotatedPdf()
    {
        // Arrange
        using var pdfStream = LoadTestPdfAsStream("valid.pdf");
        var annotationType = Domain.Models.ValueObjects.AnnotationType.TextMarkup;
        var rectangle = (X1: 100.0, Y1: 100.0, X2: 200.0, Y2: 120.0);
        var textMarkupStyle = Domain.Models.ValueObjects.TextMarkupStyle.Highlight;

        // Act
        byte[] result = await _sut.AddAnnotationAsync(
            pdfStream,
            annotationType,
            pageNumber: 1,
            rectangle,
            content: "Important text",
            author: "Test Author",
            color: "FFFF00", // Yellow
            textMarkupStyle);

        // Assert
        result.Should().NotBeEmpty();
        result.Length.Should().BeGreaterThan((int)pdfStream.Length); // Annotation adds bytes
    }

    [Fact]
    public async Task AddAnnotationAsync_FreeText_ReturnsAnnotatedPdf()
    {
        // Arrange
        using var pdfStream = LoadTestPdfAsStream("valid.pdf");
        var annotationType = Domain.Models.ValueObjects.AnnotationType.FreeText;
        var rectangle = (X1: 50.0, Y1: 50.0, X2: 150.0, Y2: 100.0);

        // Act
        byte[] result = await _sut.AddAnnotationAsync(
            pdfStream,
            annotationType,
            pageNumber: 1,
            rectangle,
            content: "Free text annotation",
            author: "John Doe",
            color: "FF0000"); // Red

        // Assert
        result.Should().NotBeEmpty();
        result.Length.Should().BeGreaterThan((int)pdfStream.Length);
    }

    [Fact]
    public async Task AddAnnotationAsync_StickyNote_ReturnsAnnotatedPdf()
    {
        // Arrange
        using var pdfStream = LoadTestPdfAsStream("valid.pdf");
        var annotationType = Domain.Models.ValueObjects.AnnotationType.StickyNote;
        var rectangle = (X1: 300.0, Y1: 300.0, X2: 320.0, Y2: 320.0);

        // Act
        byte[] result = await _sut.AddAnnotationAsync(
            pdfStream,
            annotationType,
            pageNumber: 1,
            rectangle,
            content: "Please review this section",
            author: "Reviewer");

        // Assert
        result.Should().NotBeEmpty();
    }

    [Fact]
    public async Task AddAnnotationAsync_Circle_ReturnsAnnotatedPdf()
    {
        // Arrange
        using var pdfStream = LoadTestPdfAsStream("valid.pdf");
        var annotationType = Domain.Models.ValueObjects.AnnotationType.Circle;
        var rectangle = (X1: 100.0, Y1: 200.0, X2: 200.0, Y2: 300.0);

        // Act
        byte[] result = await _sut.AddAnnotationAsync(
            pdfStream,
            annotationType,
            pageNumber: 1,
            rectangle,
            content: "Circle annotation",
            color: "00FF00"); // Green

        // Assert
        result.Should().NotBeEmpty();
    }

    [Fact]
    public async Task AddAnnotationAsync_Square_ReturnsAnnotatedPdf()
    {
        // Arrange
        using var pdfStream = LoadTestPdfAsStream("valid.pdf");
        var annotationType = Domain.Models.ValueObjects.AnnotationType.Square;
        var rectangle = (X1: 250.0, Y1: 250.0, X2: 350.0, Y2: 350.0);

        // Act
        byte[] result = await _sut.AddAnnotationAsync(
            pdfStream,
            annotationType,
            pageNumber: 1,
            rectangle,
            color: "0000FF"); // Blue

        // Assert
        result.Should().NotBeEmpty();
    }

    [Fact]
    public async Task AddAnnotationAsync_EmptyStream_ThrowsBadRequestException()
    {
        // Arrange
        using var emptyStream = new MemoryStream();

        // Act
        var act = async () => await _sut.AddAnnotationAsync(
            emptyStream,
            Domain.Models.ValueObjects.AnnotationType.FreeText,
            pageNumber: 1,
            rectangle: (0, 0, 100, 100),
            content: "Test");

        // Assert
        await act.Should().ThrowAsync<BadRequestException>()
            .WithMessage("*cannot be empty*");
    }

    [Fact]
    public async Task AddAnnotationAsync_StreamNotAtPositionZero_ThrowsBadRequestException()
    {
        // Arrange
        using var pdfStream = LoadTestPdfAsStream("valid.pdf");
        pdfStream.Position = 10; // Move stream position

        // Act
        var act = async () => await _sut.AddAnnotationAsync(
            pdfStream,
            Domain.Models.ValueObjects.AnnotationType.Circle,
            pageNumber: 1,
            rectangle: (0, 0, 100, 100));

        // Assert
        await act.Should().ThrowAsync<BadRequestException>()
            .WithMessage("*must be at position 0*");
    }

    [Fact]
    public async Task AddAnnotationAsync_InvalidPageNumber_ThrowsBadRequestException()
    {
        // Arrange
        using var pdfStream = LoadTestPdfAsStream("valid.pdf");

        // Act
        var act = async () => await _sut.AddAnnotationAsync(
            pdfStream,
            Domain.Models.ValueObjects.AnnotationType.FreeText,
            pageNumber: 999, // Exceeds page count
            rectangle: (0, 0, 100, 100),
            content: "Test");

        // Assert
        await act.Should().ThrowAsync<BadRequestException>()
            .WithMessage("*exceeds document page count*");
    }

    [Fact]
    public async Task AddAnnotationAsync_FreeTextWithoutContent_ThrowsBadRequestException()
    {
        // Arrange
        using var pdfStream = LoadTestPdfAsStream("valid.pdf");

        // Act
        var act = async () => await _sut.AddAnnotationAsync(
            pdfStream,
            Domain.Models.ValueObjects.AnnotationType.FreeText,
            pageNumber: 1,
            rectangle: (0, 0, 100, 100),
            content: null); // Missing required content

        // Assert
        await act.Should().ThrowAsync<BadRequestException>()
            .WithMessage("*requires content*");
    }

    [Fact]
    public async Task AddAnnotationAsync_StickyNoteWithoutContent_ThrowsBadRequestException()
    {
        // Arrange
        using var pdfStream = LoadTestPdfAsStream("valid.pdf");

        // Act
        var act = async () => await _sut.AddAnnotationAsync(
            pdfStream,
            Domain.Models.ValueObjects.AnnotationType.StickyNote,
            pageNumber: 1,
            rectangle: (0, 0, 20, 20),
            content: ""); // Empty content

        // Assert
        await act.Should().ThrowAsync<BadRequestException>()
            .WithMessage("*requires content*");
    }

    [Fact]
    public async Task AddAnnotationAsync_TextMarkupWithoutStyle_ThrowsBadRequestException()
    {
        // Arrange
        using var pdfStream = LoadTestPdfAsStream("valid.pdf");

        // Act
        var act = async () => await _sut.AddAnnotationAsync(
            pdfStream,
            Domain.Models.ValueObjects.AnnotationType.TextMarkup,
            pageNumber: 1,
            rectangle: (0, 0, 100, 100),
            textMarkupStyle: null); // Missing required style

        // Assert
        await act.Should().ThrowAsync<BadRequestException>()
            .WithMessage("*requires textMarkupStyle*");
    }

    [Fact]
    public async Task AddAnnotationAsync_InvalidColorFormat_ThrowsBadRequestException()
    {
        // Arrange
        using var pdfStream = LoadTestPdfAsStream("valid.pdf");

        // Act
        var act = async () => await _sut.AddAnnotationAsync(
            pdfStream,
            Domain.Models.ValueObjects.AnnotationType.Circle,
            pageNumber: 1,
            rectangle: (0, 0, 100, 100),
            color: "INVALID"); // Invalid hex format

        // Assert
        await act.Should().ThrowAsync<BadRequestException>()
            .WithMessage("*Invalid color format*");
    }

    #endregion
}

