import XCTest
import AppKit
import PDFKit
@testable import IHatePDFsCore

final class AnnotationFactoryTests: XCTestCase {
    func testHighlightSelectionRoundTripsThroughPDFSave() throws {
        let document = try makeSelectableTextDocument()
        let page = try XCTUnwrap(document.page(at: 0))
        let selection = try XCTUnwrap(page.selection(for: NSRange(location: 0, length: 29)))
        let insertions = AnnotationFactory.markupInsertions(
            from: selection,
            style: .highlight,
            comment: "Use this passage in lecture.",
            author: "Professor"
        )

        XCTAssertEqual(insertions.count, 1)

        for insertion in insertions {
            insertion.page.addAnnotation(insertion.annotation)
            if let popup = insertion.popup {
                insertion.page.addAnnotation(popup)
            }
        }

        let outputURL = FileManager.default.temporaryDirectory
            .appendingPathComponent(UUID().uuidString)
            .appendingPathExtension("pdf")
        defer { try? FileManager.default.removeItem(at: outputURL) }

        XCTAssertTrue(document.write(to: outputURL))

        let reopened = try XCTUnwrap(PDFDocument(url: outputURL))
        let reopenedPage = try XCTUnwrap(reopened.page(at: 0))
        XCTAssertEqual(reopenedPage.string, "This is selectable academic text.")
        XCTAssertTrue(
            reopenedPage.annotations.contains {
                AnnotationKeys.annotation($0, hasSubtype: .highlight)
                    && $0.contents == "Use this passage in lecture."
            }
        )
    }

    func testHighlightUsesHigherContrastDefaultColor() throws {
        let document = try makeSelectableTextDocument()
        let page = try XCTUnwrap(document.page(at: 0))
        let selection = try XCTUnwrap(page.selection(for: NSRange(location: 0, length: 29)))
        let insertion = try XCTUnwrap(
            AnnotationFactory.markupInsertions(
                from: selection,
                style: .highlight,
                comment: "",
                author: "Professor"
            ).first
        )

        let annotationColor = try rgbaComponents(insertion.annotation.color)
        let defaultColor = try rgbaComponents(AcademicAnnotationPalette.highlight)
        XCTAssertEqual(annotationColor.red, defaultColor.red, accuracy: 0.001)
        XCTAssertEqual(annotationColor.green, defaultColor.green, accuracy: 0.001)
        XCTAssertEqual(annotationColor.blue, defaultColor.blue, accuracy: 0.001)
        XCTAssertEqual(annotationColor.alpha, defaultColor.alpha, accuracy: 0.001)
        XCTAssertGreaterThanOrEqual(annotationColor.alpha, 0.5)
    }

    func testHighlightCreatedWithoutCommentHasNoPopupOrCommentText() throws {
        let document = try makeSelectableTextDocument()
        let page = try XCTUnwrap(document.page(at: 0))
        let selection = try XCTUnwrap(page.selection(for: NSRange(location: 0, length: 29)))
        let insertion = try XCTUnwrap(
            AnnotationFactory.markupInsertions(
                from: selection,
                style: .highlight,
                comment: "",
                author: "Professor"
            ).first
        )

        XCTAssertNil(insertion.popup)
        XCTAssertEqual(AnnotationKeys.commentText(for: insertion.annotation), "")
        XCTAssertNil(insertion.annotation.contents)
        XCTAssertEqual(AcademicAnnotationKind(annotation: insertion.annotation), .highlight)
    }

    func testHighlightUsesConfiguredColor() throws {
        let document = try makeSelectableTextDocument()
        let page = try XCTUnwrap(document.page(at: 0))
        let selection = try XCTUnwrap(page.selection(for: NSRange(location: 0, length: 29)))
        let configuredColor = NSColor(
            calibratedRed: 0.18,
            green: 0.58,
            blue: 0.95,
            alpha: 0.52
        )
        let insertion = try XCTUnwrap(
            AnnotationFactory.markupInsertions(
                from: selection,
                style: .highlight,
                comment: "",
                author: "Professor",
                highlightColor: configuredColor
            ).first
        )

        try XCTAssertColor(insertion.annotation.color, equals: configuredColor)
    }

    func testSelectionBoundCommentUsesConfiguredColor() throws {
        let document = try makeSelectableTextDocument()
        let page = try XCTUnwrap(document.page(at: 0))
        let selection = try XCTUnwrap(page.selection(for: NSRange(location: 8, length: 10)))
        let configuredColor = NSColor(
            calibratedRed: 0.88,
            green: 0.18,
            blue: 0.26,
            alpha: 0.34
        )
        let insertion = try XCTUnwrap(
            AnnotationFactory.markupInsertions(
                from: selection,
                style: .comment,
                comment: "",
                author: "Professor",
                commentColor: configuredColor
            ).first
        )

        try XCTAssertColor(insertion.annotation.color, equals: configuredColor)
        XCTAssertEqual(
            insertion.annotation.value(forAnnotationKey: AnnotationKeys.appKind) as? String,
            AnnotationKeys.appKindComment
        )
    }

    func testSelectionBoundCommentRoundTripsAsCommentKind() throws {
        let document = try makeSelectableTextDocument()
        let page = try XCTUnwrap(document.page(at: 0))
        let selection = try XCTUnwrap(page.selection(for: NSRange(location: 8, length: 10)))
        let insertion = try XCTUnwrap(
            AnnotationFactory.markupInsertions(
                from: selection,
                style: .comment,
                comment: "Explain this phrase.",
                author: "Professor"
            ).first
        )

        page.addAnnotation(insertion.annotation)
        if let popup = insertion.popup {
            page.addAnnotation(popup)
        }

        let reopenedPage = try saveAndReopen(document).page(at: 0).unwrap()
        let reopenedComment = try XCTUnwrap(reopenedPage.annotations.first {
            AnnotationKeys.annotation($0, hasSubtype: .highlight)
                && $0.contents == "Explain this phrase."
        })

        XCTAssertEqual(AcademicAnnotationKind(annotation: reopenedComment), .comment)
        XCTAssertEqual(
            reopenedComment.value(forAnnotationKey: AnnotationKeys.appKind) as? String,
            AnnotationKeys.appKindComment
        )
    }

    func testSelectionBoundCommentCreatedEmptyGetsPopupWhenTextIsSaved() throws {
        let document = try makeSelectableTextDocument()
        let page = try XCTUnwrap(document.page(at: 0))
        let selection = try XCTUnwrap(page.selection(for: NSRange(location: 8, length: 10)))
        let insertion = try XCTUnwrap(
            AnnotationFactory.markupInsertions(
                from: selection,
                style: .comment,
                comment: "",
                author: "Professor"
            ).first
        )

        XCTAssertNil(insertion.popup)
        page.addAnnotation(insertion.annotation)

        let popup = try XCTUnwrap(AnnotationFactory.updateComment(
            for: insertion.annotation,
            on: page,
            text: "Explain this phrase.",
            author: "Professor"
        ))
        page.addAnnotation(popup)

        let reopenedPage = try saveAndReopen(document).page(at: 0).unwrap()
        let reopenedComment = try XCTUnwrap(reopenedPage.annotations.first {
            AnnotationKeys.annotation($0, hasSubtype: .highlight)
                && $0.contents == "Explain this phrase."
        })

        XCTAssertEqual(AcademicAnnotationKind(annotation: reopenedComment), .comment)
        XCTAssertTrue(reopenedPage.annotations.contains {
            AnnotationKeys.annotation($0, hasSubtype: .popup)
                && $0.contents == "Explain this phrase."
        })
    }

    func testPopupMarkerIsPlacedInRightPageMargin() throws {
        let document = try makeSelectableTextDocument()
        let page = try XCTUnwrap(document.page(at: 0))
        let selection = try XCTUnwrap(page.selection(for: NSRange(location: 0, length: 29)))
        let insertion = try XCTUnwrap(
            AnnotationFactory.markupInsertions(
                from: selection,
                style: .comment,
                comment: "Margin marker.",
                author: "Professor"
            ).first
        )
        let popup = try XCTUnwrap(insertion.popup)
        let pageBounds = page.bounds(for: .cropBox)

        XCTAssertEqual(popup.bounds.minX, pageBounds.maxX - 28, accuracy: 0.01)
        XCTAssertGreaterThanOrEqual(popup.bounds.minY, pageBounds.minY + 12)
        XCTAssertLessThanOrEqual(popup.bounds.minY, pageBounds.maxY - 40)

        popup.bounds = insertion.annotation.bounds.offsetBy(dx: 10, dy: 0)
        XCTAssertTrue(AnnotationFactory.normalizePopupPlacement(for: insertion.annotation, on: page))
        XCTAssertEqual(popup.bounds.minX, pageBounds.maxX - 28, accuracy: 0.01)
    }

    func testDetachingPopupForViewerKeepsSidebarCommentText() throws {
        let document = try makeSelectableTextDocument()
        let page = try XCTUnwrap(document.page(at: 0))
        let selection = try XCTUnwrap(page.selection(for: NSRange(location: 0, length: 29)))
        let insertion = try XCTUnwrap(
            AnnotationFactory.markupInsertions(
                from: selection,
                style: .comment,
                comment: "",
                author: "Professor"
            ).first
        )

        page.addAnnotation(insertion.annotation)
        let popup = try XCTUnwrap(AnnotationFactory.updateComment(
            for: insertion.annotation,
            on: page,
            text: "Visible comment text.",
            author: "Professor"
        ))
        page.addAnnotation(popup)

        XCTAssertTrue(AnnotationFactory.detachPopupForViewer(from: insertion.annotation, on: page))
        XCTAssertNil(insertion.annotation.popup)
        XCTAssertNil(insertion.annotation.contents)
        XCTAssertEqual(AnnotationKeys.commentText(for: insertion.annotation), "Visible comment text.")
        XCTAssertFalse(page.annotations.contains { AnnotationKeys.annotation($0, hasSubtype: .popup) })

        let snapshot = try XCTUnwrap(AnnotationReader.snapshots(in: document).first {
            $0.annotation === insertion.annotation
        })
        XCTAssertEqual(snapshot.contents, "Visible comment text.")
    }

    func testRestoreCommentTextForExportWritesStandardPDFContents() throws {
        let document = try makeSelectableTextDocument()
        let page = try XCTUnwrap(document.page(at: 0))
        let selection = try XCTUnwrap(page.selection(for: NSRange(location: 0, length: 29)))
        let insertion = try XCTUnwrap(
            AnnotationFactory.markupInsertions(
                from: selection,
                style: .comment,
                comment: "Exported comment text.",
                author: "Professor"
            ).first
        )

        page.addAnnotation(insertion.annotation)
        if let popup = insertion.popup {
            page.addAnnotation(popup)
        }
        AnnotationFactory.detachPopupForViewer(from: insertion.annotation, on: page)

        XCTAssertNil(insertion.annotation.contents)
        XCTAssertTrue(AnnotationFactory.restoreCommentTextForExport(insertion.annotation))
        let popup = try XCTUnwrap(AnnotationFactory.makePopupIfNeeded(
            for: insertion.annotation,
            on: page,
            open: false
        ))
        if popup.page == nil {
            page.addAnnotation(popup)
        }

        let reopenedPage = try saveAndReopen(document).page(at: 0).unwrap()
        XCTAssertTrue(reopenedPage.annotations.contains {
            AnnotationKeys.annotation($0, hasSubtype: .highlight)
                && $0.contents == "Exported comment text."
        })
    }

    func testPreviewCompatibleExportKeepsMarkupCommentWithoutPopupAnnotation() throws {
        let document = try makeSelectableTextDocument()
        let page = try XCTUnwrap(document.page(at: 0))
        let selection = try XCTUnwrap(page.selection(for: NSRange(location: 0, length: 29)))
        let insertion = try XCTUnwrap(
            AnnotationFactory.markupInsertions(
                from: selection,
                style: .highlight,
                comment: "Preview should show this comment.",
                author: "Professor"
            ).first
        )

        page.addAnnotation(insertion.annotation)
        if let popup = insertion.popup {
            page.addAnnotation(popup)
        }

        XCTAssertTrue(AnnotationFactory.prepareForPreviewCompatibleExport(insertion.annotation, on: page))
        XCTAssertNil(insertion.annotation.popup)
        XCTAssertEqual(insertion.annotation.contents, "Preview should show this comment.")
        XCTAssertFalse(page.annotations.contains {
            AnnotationKeys.annotation($0, hasSubtype: .popup)
        })

        let reopenedPage = try saveAndReopen(document).page(at: 0).unwrap()
        let highlights = reopenedPage.annotations.filter {
            AnnotationKeys.annotation($0, hasSubtype: .highlight)
        }

        XCTAssertEqual(highlights.count, 1)
        XCTAssertEqual(highlights.first?.contents, "Preview should show this comment.")
        XCTAssertFalse(reopenedPage.annotations.contains {
            AnnotationKeys.annotation($0, hasSubtype: .popup)
        })
    }

    func testPreviewCompatibleExportRecoversPopupOnlyCommentText() throws {
        let document = try makeSelectableTextDocument()
        let page = try XCTUnwrap(document.page(at: 0))
        let selection = try XCTUnwrap(page.selection(for: NSRange(location: 0, length: 29)))
        let bounds = selection.bounds(for: page)
        let annotation = PDFAnnotation(bounds: bounds, forType: .highlight, withProperties: nil)
        annotation.markupType = .highlight
        annotation.color = AcademicAnnotationPalette.highlight
        annotation.userName = "Professor"
        annotation.quadrilateralPoints = [
            NSValue(point: CGPoint(x: 0, y: bounds.height)),
            NSValue(point: CGPoint(x: bounds.width, y: bounds.height)),
            NSValue(point: .zero),
            NSValue(point: CGPoint(x: bounds.width, y: 0))
        ]
        let popup = PDFAnnotation(
            bounds: CGRect(x: 360, y: 620, width: 220, height: 90),
            forType: .popup,
            withProperties: nil
        )
        popup.contents = "Popup-only comment from another reader."
        annotation.popup = popup

        page.addAnnotation(annotation)
        page.addAnnotation(popup)

        XCTAssertEqual(AnnotationKeys.commentText(for: annotation), "Popup-only comment from another reader.")
        XCTAssertTrue(AnnotationFactory.prepareForPreviewCompatibleExport(annotation, on: page))
        XCTAssertNil(annotation.popup)
        XCTAssertEqual(annotation.contents, "Popup-only comment from another reader.")
        XCTAssertFalse(page.annotations.contains {
            AnnotationKeys.annotation($0, hasSubtype: .popup)
        })
    }

    func testEmptyAppCommentTextFallsBackToStandardContents() throws {
        let annotation = PDFAnnotation(
            bounds: CGRect(x: 72, y: 620, width: 260, height: 24),
            forType: .highlight,
            withProperties: nil
        )
        AnnotationKeys.setCommentText("", for: annotation)
        annotation.contents = "Comment added by another PDF reader."

        XCTAssertEqual(AnnotationKeys.commentText(for: annotation), "Comment added by another PDF reader.")
    }

    func testEmptyAppCommentTextFallsBackToPopupContents() throws {
        let annotation = PDFAnnotation(
            bounds: CGRect(x: 72, y: 620, width: 260, height: 24),
            forType: .highlight,
            withProperties: nil
        )
        let popup = PDFAnnotation(
            bounds: CGRect(x: 360, y: 620, width: 220, height: 90),
            forType: .popup,
            withProperties: nil
        )
        AnnotationKeys.setCommentText("", for: annotation)
        popup.contents = "Popup comment added by another PDF reader."
        annotation.popup = popup

        XCTAssertEqual(AnnotationKeys.commentText(for: annotation), "Popup comment added by another PDF reader.")
    }

    func testAddingAnnotationPreservesPriorAnnotation() throws {
        let document = try makeSelectableTextDocument()
        let page = try XCTUnwrap(document.page(at: 0))

        let prior = AnnotationFactory.noteInsertion(
            on: page,
            near: CGPoint(x: 420, y: 700),
            comment: "Existing note from another reader.",
            author: "Colleague"
        )
        page.addAnnotation(prior.annotation)
        if let popup = prior.popup {
            page.addAnnotation(popup)
        }

        let selection = try XCTUnwrap(page.selection(for: NSRange(location: 8, length: 10)))
        let highlight = try XCTUnwrap(
            AnnotationFactory.markupInsertions(
                from: selection,
                style: .highlight,
                comment: "New professor comment.",
                author: "Professor"
            ).first
        )
        page.addAnnotation(highlight.annotation)
        if let popup = highlight.popup {
            page.addAnnotation(popup)
        }

        let reopenedPage = try saveAndReopen(document).page(at: 0).unwrap()
        XCTAssertEqual(reopenedPage.string, "This is selectable academic text.")
        XCTAssertTrue(reopenedPage.annotations.contains {
            AnnotationKeys.annotation($0, hasSubtype: .text)
                && $0.contents == "Existing note from another reader."
        })
        XCTAssertTrue(reopenedPage.annotations.contains {
            AnnotationKeys.annotation($0, hasSubtype: .highlight)
                && $0.contents == "New professor comment."
        })
    }

    func testScannedImagePDFCanReceiveStandardTextAnnotation() throws {
        let document = try makeImageOnlyDocument()
        let page = try XCTUnwrap(document.page(at: 0))
        let insertion = AnnotationFactory.noteInsertion(
            on: page,
            near: CGPoint(x: 300, y: 500),
            comment: "Comment on scanned reading.",
            author: "Professor"
        )
        page.addAnnotation(insertion.annotation)
        if let popup = insertion.popup {
            page.addAnnotation(popup)
        }

        let reopenedPage = try saveAndReopen(document).page(at: 0).unwrap()
        XCTAssertNil(reopenedPage.string)
        XCTAssertTrue(reopenedPage.annotations.contains {
            AnnotationKeys.annotation($0, hasSubtype: .text)
                && $0.contents == "Comment on scanned reading."
        })
    }

    func testFiveHundredPagePDFCanBeSavedWithAnnotation() throws {
        let document = PDFDocument()
        for index in 0..<501 {
            document.insert(PDFPage(), at: index)
        }
        XCTAssertEqual(document.pageCount, 501)

        let page = try XCTUnwrap(document.page(at: 500))
        let insertion = AnnotationFactory.noteInsertion(
            on: page,
            near: CGPoint(x: 120, y: 120),
            comment: "End of long reading.",
            author: "Professor"
        )
        page.addAnnotation(insertion.annotation)
        if let popup = insertion.popup {
            page.addAnnotation(popup)
        }

        let reopened = try saveAndReopen(document)
        XCTAssertEqual(reopened.pageCount, 501)
        let reopenedPage = try XCTUnwrap(reopened.page(at: 500))
        XCTAssertTrue(reopenedPage.annotations.contains {
            AnnotationKeys.annotation($0, hasSubtype: .text)
                && $0.contents == "End of long reading."
        })
    }

    func testTextAnnotationUsesStandardKeys() throws {
        let page = PDFPage()
        let insertion = AnnotationFactory.noteInsertion(
            on: page,
            near: CGPoint(x: 100, y: 100),
            comment: "Discuss this claim in class.",
            author: "Professor"
        )

        XCTAssertTrue(AnnotationKeys.annotation(insertion.annotation, hasSubtype: .text))
        XCTAssertEqual(insertion.annotation.contents, "Discuss this claim in class.")
        XCTAssertEqual(insertion.annotation.userName, "Professor")
        XCTAssertNotNil(insertion.annotation.value(forAnnotationKey: .name))
        XCTAssertNotNil(insertion.annotation.value(forAnnotationKey: AnnotationKeys.creationDate))
        XCTAssertEqual(insertion.annotation.value(forAnnotationKey: AnnotationKeys.state) as? String, "Unmarked")
        XCTAssertNotNil(insertion.popup)
        XCTAssertTrue(insertion.popup.map { AnnotationKeys.annotation($0, hasSubtype: .popup) } ?? false)
    }

    func testReplyStoresHiddenTextAnnotationWithBestEffortParentID() throws {
        let page = PDFPage()
        let parent = AnnotationFactory.noteInsertion(
            on: page,
            near: CGPoint(x: 100, y: 100),
            comment: "Parent",
            author: "Professor"
        ).annotation
        let reply = AnnotationFactory.replyInsertion(
            to: parent,
            on: page,
            comment: "Reply",
            author: "Reader",
            parentID: "parent-id"
        )

        XCTAssertTrue(AnnotationKeys.annotation(reply.annotation, hasSubtype: .text))
        XCTAssertEqual(reply.annotation.value(forAnnotationKey: AnnotationKeys.inReplyTo) as? String, "parent-id")
        XCTAssertEqual(reply.annotation.value(forAnnotationKey: AnnotationKeys.replyType) as? String, "R")
        XCTAssertFalse(reply.annotation.shouldDisplay)
        XCTAssertFalse(reply.annotation.shouldPrint)

        page.addAnnotation(reply.annotation)
        AnnotationFactory.hideReplyMarker(reply.annotation, on: page)
        XCTAssertFalse(reply.annotation.shouldDisplay)
        XCTAssertFalse(reply.annotation.shouldPrint)
        XCTAssertEqual(AnnotationKeys.commentText(for: reply.annotation), "Reply")
        XCTAssertGreaterThan(reply.annotation.bounds.minX, page.bounds(for: .cropBox).maxX)
        XCTAssertGreaterThan(reply.annotation.bounds.minY, page.bounds(for: .cropBox).maxY)
        XCTAssertNil(reply.popup)
    }

    func testStringReplyParentIDResolvesToParentStableID() throws {
        let document = PDFDocument()
        let page = PDFPage()
        document.insert(page, at: 0)

        let parent = AnnotationFactory.noteInsertion(
            on: page,
            near: CGPoint(x: 100, y: 100),
            comment: "Parent",
            author: "Professor"
        ).annotation
        page.addAnnotation(parent)

        let parentID = try XCTUnwrap(parent.value(forAnnotationKey: .name) as? String)
        let reply = AnnotationFactory.replyInsertion(
            to: parent,
            on: page,
            comment: "Reply",
            author: "Reader",
            parentID: parentID
        ).annotation
        page.addAnnotation(reply)
        AnnotationFactory.hideReplyMarker(reply, on: page)

        let snapshots = AnnotationReader.snapshots(in: document)
        let parentSnapshot = try XCTUnwrap(snapshots.first { $0.contents == "Parent" })
        let replySnapshot = try XCTUnwrap(snapshots.first { $0.contents == "Reply" })

        XCTAssertEqual(replySnapshot.kind, .reply)
        XCTAssertEqual(replySnapshot.parentID, parentSnapshot.id)
    }

    func testUnresolvedStringReplyParentIDStaysVisibleAsTopLevelReply() throws {
        let document = PDFDocument()
        let page = PDFPage()
        document.insert(page, at: 0)

        let orphanedReply = PDFAnnotation(
            bounds: CGRect(x: 100, y: 100, width: 24, height: 24),
            forType: .text,
            withProperties: nil
        )
        AnnotationFactory.standardize(
            orphanedReply,
            comment: "Reply from an external reader.",
            author: "Reader",
            date: Date()
        )
        _ = orphanedReply.setValue("missing-parent", forAnnotationKey: AnnotationKeys.inReplyTo)
        _ = orphanedReply.setValue("R", forAnnotationKey: AnnotationKeys.replyType)
        page.addAnnotation(orphanedReply)

        let snapshot = try XCTUnwrap(AnnotationReader.snapshots(in: document).first)
        XCTAssertEqual(snapshot.kind, .reply)
        XCTAssertNil(snapshot.parentID)
        XCTAssertFalse(snapshot.isReply)
        XCTAssertEqual(snapshot.contents, "Reply from an external reader.")
    }

    func testPageScopedSnapshotsOnlyReadRequestedPages() throws {
        let document = PDFDocument()
        let firstPage = PDFPage()
        let secondPage = PDFPage()
        let thirdPage = PDFPage()
        document.insert(firstPage, at: 0)
        document.insert(secondPage, at: 1)
        document.insert(thirdPage, at: 2)

        let firstAnnotation = AnnotationFactory.noteInsertion(
            on: firstPage,
            near: CGPoint(x: 100, y: 100),
            comment: "First page note",
            author: "Professor"
        ).annotation
        firstPage.addAnnotation(firstAnnotation)

        let thirdAnnotation = AnnotationFactory.noteInsertion(
            on: thirdPage,
            near: CGPoint(x: 200, y: 200),
            comment: "Third page note",
            author: "Professor"
        ).annotation
        thirdPage.addAnnotation(thirdAnnotation)

        let scopedSnapshots = AnnotationReader.snapshots(in: document, pages: [thirdPage, thirdPage])

        XCTAssertEqual(scopedSnapshots.count, 1)
        XCTAssertEqual(scopedSnapshots.first?.contents, "Third page note")
        XCTAssertEqual(scopedSnapshots.first?.pageIndex, 2)
        XCTAssertFalse(scopedSnapshots.contains { $0.annotation === firstAnnotation })
    }

    func testFreeTextCreatesStandardFreeTextAnnotation() throws {
        let page = PDFPage()
        let insertion = AnnotationFactory.freeTextInsertion(
            on: page,
            near: CGPoint(x: 200, y: 200),
            text: "Important definition",
            author: "Professor"
        )

        XCTAssertTrue(AnnotationKeys.annotation(insertion.annotation, hasSubtype: .freeText))
        XCTAssertEqual(insertion.annotation.contents, "Important definition")
        XCTAssertNil(insertion.popup)
    }

    private func makeSelectableTextDocument() throws -> PDFDocument {
        let data = NSMutableData()
        var mediaBox = CGRect(x: 0, y: 0, width: 612, height: 792)
        let consumer = try XCTUnwrap(CGDataConsumer(data: data))
        let context = try XCTUnwrap(CGContext(consumer: consumer, mediaBox: &mediaBox, nil))

        context.beginPDFPage(nil)
        NSGraphicsContext.saveGraphicsState()
        NSGraphicsContext.current = NSGraphicsContext(cgContext: context, flipped: false)
        let text = NSAttributedString(
            string: "This is selectable academic text.",
            attributes: [.font: NSFont.systemFont(ofSize: 18)]
        )
        text.draw(at: CGPoint(x: 72, y: 700))
        NSGraphicsContext.restoreGraphicsState()
        context.endPDFPage()
        context.closePDF()

        return try XCTUnwrap(PDFDocument(data: data as Data))
    }

    private func makeImageOnlyDocument() throws -> PDFDocument {
        let image = NSImage(size: CGSize(width: 612, height: 792))
        image.lockFocus()
        NSColor.white.setFill()
        NSRect(x: 0, y: 0, width: 612, height: 792).fill()
        NSColor.darkGray.setStroke()
        let path = NSBezierPath(rect: NSRect(x: 72, y: 580, width: 468, height: 80))
        path.lineWidth = 2
        path.stroke()
        image.unlockFocus()

        let document = PDFDocument()
        document.insert(try XCTUnwrap(PDFPage(image: image)), at: 0)
        return document
    }

    private func saveAndReopen(_ document: PDFDocument) throws -> PDFDocument {
        let outputURL = FileManager.default.temporaryDirectory
            .appendingPathComponent(UUID().uuidString)
            .appendingPathExtension("pdf")
        XCTAssertTrue(document.write(to: outputURL))
        let reopened = try XCTUnwrap(PDFDocument(url: outputURL))
        try? FileManager.default.removeItem(at: outputURL)
        return reopened
    }

    private func XCTAssertColor(
        _ actual: NSColor,
        equals expected: NSColor,
        file: StaticString = #filePath,
        line: UInt = #line
    ) throws {
        let actualComponents = try rgbaComponents(actual, file: file, line: line)
        let expectedComponents = try rgbaComponents(expected, file: file, line: line)

        XCTAssertEqual(actualComponents.red, expectedComponents.red, accuracy: 0.001, file: file, line: line)
        XCTAssertEqual(actualComponents.green, expectedComponents.green, accuracy: 0.001, file: file, line: line)
        XCTAssertEqual(actualComponents.blue, expectedComponents.blue, accuracy: 0.001, file: file, line: line)
        XCTAssertEqual(actualComponents.alpha, expectedComponents.alpha, accuracy: 0.001, file: file, line: line)
    }

    private func rgbaComponents(
        _ color: NSColor,
        file: StaticString = #filePath,
        line: UInt = #line
    ) throws -> (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) {
        let rgb = try XCTUnwrap(color.usingColorSpace(.deviceRGB), file: file, line: line)
        var red: CGFloat = 0
        var green: CGFloat = 0
        var blue: CGFloat = 0
        var alpha: CGFloat = 0
        rgb.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
        return (red, green, blue, alpha)
    }
}

private extension Optional {
    func unwrap(
        file: StaticString = #filePath,
        line: UInt = #line
    ) throws -> Wrapped {
        try XCTUnwrap(self, file: file, line: line)
    }
}
