import AppKit
import Foundation
import Testing
@testable import Kanban

/// Printing, reduced to the rules a unit test can hold (11-command-nexus.md ▸ File ▸ Print…).
///
/// The typography is not the point and is not tested — that is `PrintDocumentRenderer`'s and it is
/// legitimately unverifiable without eyes, the same standing `BodyMarkupRenderer` has. What *is* tested is
/// every layer under it, each of which fails silently:
///
/// - **Profiles** round-trip through a preferences plist. A field that stopped encoding, or a decoder that
///   threw on a shape a future build wrote, costs the user every profile they saved — and nothing on screen
///   would say so until they looked for a profile that had gone.
/// - **The document's structure** is where every option actually lands: which components appear, in what
///   order, which end of a thread comes first, and where a page break falls. All four are invisible in a
///   rendered page and obvious in a block list.
/// - **The `labels` reading** interprets a key 01-storage-format.md reserves and this version otherwise
///   leaves inert, so printing is the one surface that can be wrong about it.
/// - **The trash exclusion** is the one rule whose failure would print deleted cards.

// MARK: - Helpers

/// A profile store on a scratch defaults domain — profiles are app-wide and persisted, so a suite that used
/// `.standard` would rewrite the developer's own (`BoardZoomTests`' arrangement, verbatim in intent).
@MainActor
private func makeStore() -> (PrintProfileStore, UserDefaults, () -> Void) {
    let name = "dev.rzen.indie.Kanban.print-tests.\(UUID().uuidString)"
    let defaults = UserDefaults(suiteName: name)!
    return (PrintProfileStore(defaults: defaults), defaults, {
        UserDefaults.standard.removePersistentDomain(forName: name)
    })
}

/// Options with every field moved off its default, so a round-trip that dropped one is visible.
private func exoticOptions() -> PrintOptions {
    var options = PrintOptions()
    options.includesTitle = false
    options.includesLabels = false
    options.includesBody = false
    options.includesComments = true
    options.commentSort = .newestFirst
    options.pageBreaks = .betweenCards
    options.fontFamily = "Palatino"
    options.fontSize = 13.5
    options.headerShowsBoardTitle = false
    options.headerShowsPrintDate = false
    options.footerShowsPageNumbers = false
    options.footerShowsCustomLine = true
    options.footerCustomLine = "Confidential"
    return options
}

private func card(
    _ title: String?,
    body: String = "",
    icon: String? = nil,
    labels: [String] = [],
    comments: [PrintComment] = []
) -> PrintCard {
    PrintCard(title: title, icon: icon, labels: labels, body: body, comments: comments)
}

private func board(_ lanes: [PrintLane], titled title: String = "Roadmap") -> PrintSource {
    PrintSource(scope: .board, boardTitle: title, lanes: lanes)
}

/// A date some fixed distance from a base, so comment ordering is a fact about the sort rather than about
/// the clock.
private func stamp(_ minutes: Int) -> Date {
    Date(timeIntervalSince1970: 1_760_000_000).addingTimeInterval(TimeInterval(minutes * 60))
}

// MARK: - Options ▸ Codable

@Suite("Print ▸ options round-trip")
struct PrintOptionsCodableTests {

    @Test("Every field survives an encode and a decode")
    func roundTrips() throws {
        let options = exoticOptions()
        let decoded = try JSONDecoder().decode(PrintOptions.self, from: try JSONEncoder().encode(options))
        #expect(decoded == options)
    }

    @Test("The defaults round-trip too — the state a first print is configured in")
    func defaultsRoundTrip() throws {
        let options = PrintOptions()
        let decoded = try JSONDecoder().decode(PrintOptions.self, from: try JSONEncoder().encode(options))
        #expect(decoded == options)
        #expect(decoded.includesComments == false, "comments are off by default")
        #expect(decoded.pageBreaks == .flow)
        #expect(decoded.fontFamily == nil, "the system font is the absence of a family, not a spelling of one")
    }

    /// The failure mode the lenient decoder exists to rule out: one unrecognized shape must not cost the
    /// whole value.
    @Test("A stored shape from another build decodes to the defaults rather than throwing")
    func toleratesForeignShapes() throws {
        let json = """
        {"includesTitle": false, "pageBreaks": "betweenParagraphs", "commentSort": 7,
         "fontSize": "large", "somethingNew": {"a": 1}}
        """
        let decoded = try JSONDecoder().decode(PrintOptions.self, from: Data(json.utf8))

        #expect(decoded.includesTitle == false, "the field it did understand is honoured")
        #expect(decoded.pageBreaks == .flow, "an unknown page-break mode reads as the default")
        #expect(decoded.commentSort == .oldestFirst, "a wrongly-typed sort reads as the default")
        #expect(decoded.fontSize == PrintOptions().fontSize, "a wrongly-typed size reads as the default")
        #expect(decoded.includesBody, "every absent field keeps its default")
    }

    @Test("A stored size outside the legal range is clamped, never drawn")
    func clampsSize() throws {
        for (stored, expected) in [(0.0, PrintOptions.fontSizeRange.lowerBound),
                                   (-4.0, PrintOptions.fontSizeRange.lowerBound),
                                   (900.0, PrintOptions.fontSizeRange.upperBound)] {
            let decoded = try JSONDecoder().decode(
                PrintOptions.self,
                from: Data("{\"fontSize\": \(stored)}".utf8)
            )
            #expect(decoded.fontSize == expected)
        }
    }

    @Test("Normalizing is a reading, not a rewrite — it never breaks the round-trip")
    func normalizingIsARead() throws {
        var options = PrintOptions()
        options.footerShowsCustomLine = true
        options.footerCustomLine = "   "

        // The stored value keeps the toggle: a banner emptied for one print and typed back in for the next
        // must not lose it.
        let decoded = try JSONDecoder().decode(PrintOptions.self, from: try JSONEncoder().encode(options))
        #expect(decoded == options)
        // The render's reading drops it.
        #expect(decoded.normalized.footerShowsCustomLine == false)
    }

    @Test("Three components off and comments off is 'nothing to print'")
    func describesAnyContent() {
        var options = PrintOptions()
        #expect(options.describesAnyContent)
        options.includesTitle = false
        options.includesLabels = false
        options.includesBody = false
        #expect(!options.describesAnyContent)
        options.includesComments = true
        #expect(options.describesAnyContent)
    }
}

// MARK: - The catalog's rules

@Suite("Print ▸ the profile catalog")
struct PrintProfileCatalogTests {

    @Test("A save appends; a save over the same name overwrites in place")
    func savingAndOverwriting() {
        var catalog = PrintProfileCatalog()
        catalog.save(PrintOptions(), as: "Handout")
        var second = PrintOptions()
        second.fontSize = 20
        catalog.save(second, as: "Archive")

        #expect(catalog.names == ["Handout", "Archive"], "list order is the order they were saved in")

        var replacement = PrintOptions()
        replacement.pageBreaks = .betweenLanes
        catalog.save(replacement, as: "Handout")

        #expect(catalog.names == ["Handout", "Archive"], "an overwrite does not move the row")
        #expect(catalog.options(named: "Handout")?.pageBreaks == .betweenLanes)
    }

    @Test("Names compare case-insensitively, and the last spelling typed wins")
    func namesFoldCase() {
        var catalog = PrintProfileCatalog()
        catalog.save(PrintOptions(), as: "Handout")
        catalog.save(PrintOptions(), as: "  handout  ")

        #expect(catalog.names == ["handout"], "one profile, spelled the way it was last saved")
        #expect(catalog.contains("HANDOUT"))
    }

    @Test("The reserved name cannot be claimed, in any spelling")
    func reservedNameRefused() {
        var catalog = PrintProfileCatalog()
        let reserved = catalog.save(PrintOptions(), as: PrintProfile.lastUsedName)
        let folded = catalog.save(PrintOptions(), as: "last used")
        let blank = catalog.save(PrintOptions(), as: "   ")
        #expect(reserved == false)
        #expect(folded == false)
        #expect(blank == false, "a blank name is not a name")
        #expect(catalog.names.isEmpty)
    }

    @Test("A rename keeps position and options; a collision is refused")
    func renaming() {
        var catalog = PrintProfileCatalog()
        var handout = PrintOptions()
        handout.fontSize = 15
        catalog.save(handout, as: "Handout")
        catalog.save(PrintOptions(), as: "Archive")

        let renamed = catalog.rename("Handout", to: "Standup")
        #expect(renamed)
        #expect(catalog.names == ["Standup", "Archive"], "position survives the rename")
        #expect(catalog.options(named: "Standup")?.fontSize == 15)

        let collision = catalog.rename("Standup", to: "Archive")
        let reserved = catalog.rename("Standup", to: PrintProfile.lastUsedName)
        let missing = catalog.rename("Nothing", to: "Something")
        let recased = catalog.rename("Standup", to: "STANDUP")
        #expect(collision == false, "a rename never swallows a sibling")
        #expect(reserved == false)
        #expect(missing == false)
        #expect(recased, "re-casing its own name is allowed")
        #expect(catalog.names == ["STANDUP", "Archive"])
    }

    @Test("A delete of a name that matches nothing is a no-op")
    func deleting() {
        var catalog = PrintProfileCatalog()
        catalog.save(PrintOptions(), as: "Handout")
        catalog.delete("Nothing")
        #expect(catalog.names == ["Handout"])
        catalog.delete("handout")
        #expect(catalog.names.isEmpty)
    }

    /// The bytes come out of a preferences plist a human may have edited, so the sanitizing rule is what a
    /// menu can render rather than a guarantee about the writer.
    @Test("Construction drops blank, reserved and duplicate entries")
    func sanitizesOnConstruction() {
        let catalog = PrintProfileCatalog(profiles: [
            PrintProfile(name: "  Handout  ", options: PrintOptions()),
            PrintProfile(name: "", options: PrintOptions()),
            PrintProfile(name: PrintProfile.lastUsedName, options: PrintOptions()),
            PrintProfile(name: "handout", options: PrintOptions())
        ])
        #expect(catalog.names == ["Handout"], "trimmed, and the first of two spellings kept")
    }

    @Test("A catalog round-trips, and one malformed entry does not cost the rest")
    func catalogCodable() throws {
        var catalog = PrintProfileCatalog()
        catalog.save(exoticOptions(), as: "Archive")
        catalog.save(PrintOptions(), as: "Handout")

        let decoded = try JSONDecoder().decode(PrintProfileCatalog.self, from: try JSONEncoder().encode(catalog))
        #expect(decoded == catalog)

        let partial = """
        {"profiles": [{"name": "Kept"}, {"options": {"fontSize": 12}}, {"name": "Also kept", "options": {}}]}
        """
        let lenient = try JSONDecoder().decode(PrintProfileCatalog.self, from: Data(partial.utf8))
        #expect(lenient.names == ["Kept", "Also kept"], "the nameless entry is dropped, the rest survive")
        #expect(lenient.options(named: "Kept") == PrintOptions())
    }
}

// MARK: - The store's persistence

@Suite("Print ▸ profile persistence")
@MainActor
struct PrintProfileStoreTests {

    @Test("A saved profile survives a fresh store over the same domain")
    func savePersists() {
        let (store, defaults, teardown) = makeStore()
        defer { teardown() }

        #expect(store.catalog.names.isEmpty, "a first launch has no profiles")
        #expect(store.save(exoticOptions(), as: "Archive"))

        let reopened = PrintProfileStore(defaults: defaults)
        #expect(reopened.catalog.names == ["Archive"])
        #expect(reopened.options(named: "Archive") == exoticOptions())
    }

    @Test("Renames and deletes persist too")
    func managementPersists() {
        let (store, defaults, teardown) = makeStore()
        defer { teardown() }

        store.save(PrintOptions(), as: "Handout")
        store.save(exoticOptions(), as: "Archive")
        #expect(store.rename("Handout", to: "Standup"))
        store.delete("Archive")

        #expect(PrintProfileStore(defaults: defaults).catalog.names == ["Standup"])
    }

    @Test("Last Used captures the options a print ran with, and survives a relaunch")
    func lastUsedCaptures() {
        let (store, defaults, teardown) = makeStore()
        defer { teardown() }

        #expect(store.lastUsed == nil, "nothing has been printed yet")
        #expect(store.options(named: PrintProfile.lastUsedName) == PrintOptions(),
                "the reserved row reads as the factory defaults on a first launch")

        store.captureLastUsed(exoticOptions())
        #expect(store.lastUsed == exoticOptions())

        let reopened = PrintProfileStore(defaults: defaults)
        #expect(reopened.lastUsed == exoticOptions())
        #expect(reopened.options(named: PrintProfile.lastUsedName) == exoticOptions())
    }

    @Test("The reserved row leads the menu, then the named ones in save order")
    func menuOrder() {
        let (store, _, teardown) = makeStore()
        defer { teardown() }

        store.save(PrintOptions(), as: "Handout")
        store.save(PrintOptions(), as: "Archive")
        #expect(store.menuNames == [PrintProfile.lastUsedName, "Handout", "Archive"])
    }

    @Test("A garbage preference reads as nothing stored rather than taking the surface down")
    func toleratesGarbage() throws {
        let name = "dev.rzen.indie.Kanban.print-garbage.\(UUID().uuidString)"
        let defaults = try #require(UserDefaults(suiteName: name))
        defer { UserDefaults.standard.removePersistentDomain(forName: name) }

        defaults.set(42, forKey: AppPreferences.printProfilesKey)
        defaults.set(Data("not json".utf8), forKey: AppPreferences.printLastUsedKey)

        let store = PrintProfileStore(defaults: defaults)
        #expect(store.catalog.names.isEmpty)
        #expect(store.lastUsed == nil)
    }

    @Test("A refused name writes nothing at all")
    func refusalWritesNothing() {
        let (store, defaults, teardown) = makeStore()
        defer { teardown() }

        #expect(store.save(PrintOptions(), as: PrintProfile.lastUsedName) == false)
        #expect(defaults.data(forKey: AppPreferences.printProfilesKey) == nil)
    }
}

// MARK: - The document

@Suite("Print ▸ document assembly")
struct PrintDocumentBuilderTests {

    private static let source = board([
        PrintLane(title: "Doing", cards: [
            card("Fix login", body: "Some **words**.", icon: "flag", labels: ["bug", "ui"]),
            card("Ship it", body: "More words.")
        ]),
        PrintLane(title: "Done", cards: [
            card("Old thing", body: "Done words.")
        ])
    ])

    @Test("The default document is board, lane, card, meta, body — in that order")
    func defaultOrder() {
        let blocks = PrintDocumentBuilder.blocks(from: Self.source, options: PrintOptions())

        #expect(blocks == [
            .boardHeading("Roadmap"),
            .laneHeading("Doing"),
            .cardTitle("Fix login"),
            .cardMeta(icon: "flag", labels: ["bug", "ui"]),
            .cardBody("Some **words**."),
            .cardTitle("Ship it"),
            .cardBody("More words."),
            .laneHeading("Done"),
            .cardTitle("Old thing"),
            .cardBody("Done words.")
        ])
    }

    @Test("A card with no chosen icon and no labels contributes no meta line")
    func metaOmittedWhenEmpty() {
        let blocks = PrintDocumentBuilder.blocks(from: Self.source, options: PrintOptions())
        let metas = blocks.filter { if case .cardMeta = $0 { return true } else { return false } }
        #expect(metas.count == 1, "only the card that has something to say gets the line")
    }

    @Test("Each component toggle removes exactly its own block")
    func componentToggles() {
        var options = PrintOptions()
        options.includesTitle = false
        var blocks = PrintDocumentBuilder.blocks(from: Self.source, options: options)
        #expect(!blocks.contains { if case .cardTitle = $0 { return true } else { return false } })
        #expect(blocks.contains { if case .cardBody = $0 { return true } else { return false } })

        options = PrintOptions()
        options.includesLabels = false
        blocks = PrintDocumentBuilder.blocks(from: Self.source, options: options)
        #expect(!blocks.contains { if case .cardMeta = $0 { return true } else { return false } })

        options = PrintOptions()
        options.includesBody = false
        blocks = PrintDocumentBuilder.blocks(from: Self.source, options: options)
        #expect(!blocks.contains { if case .cardBody = $0 { return true } else { return false } })
        #expect(blocks.contains { if case .cardTitle = $0 { return true } else { return false } })
    }

    @Test("Every component off prints nothing — not a page of running heads")
    func nothingIncludedIsAnEmptyDocument() {
        var options = PrintOptions()
        options.includesTitle = false
        options.includesLabels = false
        options.includesBody = false
        #expect(PrintDocumentBuilder.blocks(from: Self.source, options: options).isEmpty)
    }

    @Test("An untitled card and an untitled lane print the placeholder, never a blank line")
    func untitledPlaceholder() {
        let source = board([PrintLane(title: nil, cards: [card(nil, body: "Words.")])])
        let blocks = PrintDocumentBuilder.blocks(from: source, options: PrintOptions())
        #expect(blocks == [
            .boardHeading("Roadmap"),
            .laneHeading(PrintDocumentBuilder.untitled),
            .cardTitle(PrintDocumentBuilder.untitled),
            .cardBody("Words.")
        ])
    }

    /// The whitespace case is the card window's own emptiness rule (`BodyMarkup.isEmpty`), reused so a body
    /// of one newline does not print a blank paragraph.
    @Test("A whitespace-only body is no body")
    func whitespaceBodyOmitted() {
        let source = board([PrintLane(title: "Doing", cards: [card("Titled", body: "\n  \n")])])
        let blocks = PrintDocumentBuilder.blocks(from: source, options: PrintOptions())
        #expect(blocks == [.boardHeading("Roadmap"), .laneHeading("Doing"), .cardTitle("Titled")])
    }

    @Test("An empty lane is omitted, and so is a card with nothing to show")
    func emptiesDropOut() {
        let source = board([
            PrintLane(title: "Empty", cards: []),
            PrintLane(title: "All blank", cards: [card(nil), card(nil)]),
            PrintLane(title: "Real", cards: [card("Kept", body: "Words.")])
        ])
        var options = PrintOptions()
        // With titles off, the two blank cards have nothing left at all — which must take their lane with
        // them rather than leaving a heading over nothing.
        options.includesTitle = false

        let blocks = PrintDocumentBuilder.blocks(from: source, options: options)
        #expect(blocks == [.boardHeading("Roadmap"), .laneHeading("Real"), .cardBody("Words.")])
    }

    @Test("A card print is the card — no board heading, no lane heading")
    func cardScope() {
        let source = PrintSource(
            scope: .card,
            boardTitle: "Roadmap",
            lanes: [PrintLane(title: "Doing", cards: [card("Fix login", body: "Words.")])]
        )
        var options = PrintOptions()
        options.pageBreaks = .betweenCards

        #expect(PrintDocumentBuilder.blocks(from: source, options: options) == [
            .cardTitle("Fix login"),
            .cardBody("Words.")
        ])
    }
}

// MARK: - Page breaks

@Suite("Print ▸ page breaks")
struct PrintPageBreakTests {

    private static let source = board([
        PrintLane(title: "Doing", cards: [card("A", body: "a"), card("B", body: "b")]),
        PrintLane(title: "Done", cards: [card("C", body: "c")])
    ])

    @Test("Continuous emits no break at all")
    func flow() {
        let blocks = PrintDocumentBuilder.blocks(from: Self.source, options: PrintOptions())
        #expect(!blocks.contains(.pageBreak))
    }

    @Test("Between lanes breaks before each lane after the first, and nowhere else")
    func betweenLanes() {
        var options = PrintOptions()
        options.pageBreaks = .betweenLanes

        #expect(PrintDocumentBuilder.blocks(from: Self.source, options: options) == [
            .boardHeading("Roadmap"),
            .laneHeading("Doing"),
            .cardTitle("A"), .cardBody("a"),
            .cardTitle("B"), .cardBody("b"),
            .pageBreak,
            .laneHeading("Done"),
            .cardTitle("C"), .cardBody("c")
        ])
    }

    @Test("Between cards breaks before every card but the document's first")
    func betweenCards() {
        var options = PrintOptions()
        options.pageBreaks = .betweenCards

        #expect(PrintDocumentBuilder.blocks(from: Self.source, options: options) == [
            .boardHeading("Roadmap"),
            .laneHeading("Doing"),
            .cardTitle("A"), .cardBody("a"),
            .pageBreak,
            .cardTitle("B"), .cardBody("b"),
            .pageBreak,
            .laneHeading("Done"),
            .cardTitle("C"), .cardBody("c")
        ])
    }

    /// The board's name is the first lane's running-in title, not a title page — the one thing that would
    /// otherwise put a lone heading on sheet one of every print with breaks on.
    @Test("The board heading never earns a break of its own")
    func boardHeadingIsNotATitlePage() {
        for mode in [PrintPageBreaks.betweenLanes, .betweenCards] {
            var options = PrintOptions()
            options.pageBreaks = mode
            let blocks = PrintDocumentBuilder.blocks(from: Self.source, options: options)
            #expect(blocks.first == .boardHeading("Roadmap"))
            #expect(blocks.dropFirst().first == .laneHeading("Doing"), "no break between the two")
        }
    }

    @Test("A break is never leading, never trailing, and never doubled")
    func breaksAreWellFormed() {
        let source = board([
            PrintLane(title: "Empty", cards: []),
            PrintLane(title: "One", cards: [card("A", body: "a")]),
            PrintLane(title: "Blank", cards: [card(nil)]),
            PrintLane(title: "Two", cards: [card("B", body: "b")])
        ])
        var options = PrintOptions()
        options.pageBreaks = .betweenCards
        options.includesTitle = false

        let blocks = PrintDocumentBuilder.blocks(from: source, options: options)
        #expect(blocks.first != .pageBreak)
        #expect(blocks.last != .pageBreak)
        for (left, right) in zip(blocks, blocks.dropFirst()) {
            #expect(!(left == .pageBreak && right == .pageBreak))
        }
        #expect(blocks.filter { $0 == .pageBreak }.count == 1, "the two dropped lanes take their breaks too")
    }

    @Test("A single-card board never breaks, whatever the mode")
    func oneCardNeverBreaks() {
        let source = board([PrintLane(title: "Doing", cards: [card("A", body: "a")])])
        for mode in PrintPageBreaks.allCases {
            var options = PrintOptions()
            options.pageBreaks = mode
            #expect(!PrintDocumentBuilder.blocks(from: source, options: options).contains(.pageBreak))
        }
    }
}

// MARK: - Comments

@Suite("Print ▸ comments")
struct PrintCommentTests {

    private static let thread = [
        PrintComment(author: "ada", created: stamp(0), body: "first"),
        PrintComment(author: "grace", created: stamp(10), body: "second"),
        PrintComment(author: nil, created: stamp(20), body: "third")
    ]

    private static let source = board([
        PrintLane(title: "Doing", cards: [card("A", body: "a", comments: Self.thread)])
    ])

    @Test("Comments are off by default and print nothing")
    func offByDefault() {
        let blocks = PrintDocumentBuilder.blocks(from: Self.source, options: PrintOptions())
        #expect(!blocks.contains { if case .comment = $0 { return true } else { return false } })
        #expect(!blocks.contains { if case .commentsHeading = $0 { return true } else { return false } })
    }

    @Test("Oldest first walks the thread's own chronology, heading first")
    func oldestFirst() {
        var options = PrintOptions()
        options.includesComments = true

        #expect(PrintDocumentBuilder.blocks(from: Self.source, options: options) == [
            .boardHeading("Roadmap"),
            .laneHeading("Doing"),
            .cardTitle("A"),
            .cardBody("a"),
            .commentsHeading(count: 3),
            .comment(author: "ada", created: stamp(0), body: "first"),
            .comment(author: "grace", created: stamp(10), body: "second"),
            .comment(author: nil, created: stamp(20), body: "third")
        ])
    }

    @Test("Newest first reverses that order and nothing else")
    func newestFirst() {
        var options = PrintOptions()
        options.includesComments = true
        options.commentSort = .newestFirst

        let bodies = PrintDocumentBuilder.blocks(from: Self.source, options: options).compactMap { block -> String? in
            guard case let .comment(_, _, body) = block else { return nil }
            return body
        }
        #expect(bodies == ["third", "second", "first"])
    }

    @Test("A card with no comments gets no heading, even with comments on")
    func emptyThread() {
        var options = PrintOptions()
        options.includesComments = true
        let source = board([PrintLane(title: "Doing", cards: [card("A", body: "a")])])
        #expect(!PrintDocumentBuilder.blocks(from: source, options: options)
            .contains { if case .commentsHeading = $0 { return true } else { return false } })
    }

    /// A thread is the only content a card may have — with the title, the labels and the body all off, the
    /// comments still print.
    @Test("Comments alone are enough to keep a card in the document")
    func commentsAloneKeepACard() {
        var options = PrintOptions()
        options.includesTitle = false
        options.includesLabels = false
        options.includesBody = false
        options.includesComments = true

        #expect(PrintDocumentBuilder.blocks(from: Self.source, options: options) == [
            .boardHeading("Roadmap"),
            .laneHeading("Doing"),
            .commentsHeading(count: 3),
            .comment(author: "ada", created: stamp(0), body: "first"),
            .comment(author: "grace", created: stamp(10), body: "second"),
            .comment(author: nil, created: stamp(20), body: "third")
        ])
    }

    @Test("A thread flattens in the loader's order — printing never re-sorts")
    func flatteningKeepsThreadOrder() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("", "---\nschema: 1\ntitle: Board\n---\n")
        let lane = "11111111-1111-4111-8111-111111111111"
        let cardID = "22222222-2222-4222-8222-222222222222"
        try fixture.item(lane, "---\nschema: 1\ntitle: Doing\norder: 1024\n---\n")
        try fixture.item("\(lane)/\(cardID)", "---\nschema: 1\ntitle: A\norder: 1024\n---\nBody.\n")
        // Deliberately written out of chronological order, so an order that came out right could only have
        // come from the sort.
        try fixture.item(
            "\(lane)/\(cardID)/comments/33333333-3333-4333-8333-333333333333",
            "---\nschema: 1\nkind: comment\nauthor: grace\ncreated: 2026-02-02T09:00:00Z\n---\nsecond\n"
        )
        try fixture.item(
            "\(lane)/\(cardID)/comments/44444444-4444-4444-8444-444444444444",
            "---\nschema: 1\nkind: comment\nauthor: ada\ncreated: 2026-01-01T09:00:00Z\n---\nfirst\n"
        )

        let cardFolder = fixture.url("\(lane)/\(cardID)")
        let comments = PrintComment.list(of: CommentThread.load(inCard: cardFolder, path: "\(lane)/\(cardID)"))
        #expect(comments.map(\.body) == ["first\n", "second\n"])
        #expect(comments.map(\.author) == ["ada", "grace"])
    }
}

// MARK: - Extraction from a real board

@Suite("Print ▸ what a board contributes")
@MainActor
struct PrintSourceTests {

    /// A board with two lanes, a trashed card and a trashed lane — the one arrangement whose failure would
    /// print deleted cards.
    private func fixture() throws -> WriterFixture {
        let fixture = try WriterFixture()
        try fixture.item("", "---\nschema: 1\ntitle: Roadmap\n---\n")
        try fixture.item("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", "---\nschema: 1\ntitle: Doing\norder: 1024\n---\n")
        try fixture.item("bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", "---\nschema: 1\ntitle: Done\norder: 2048\n---\n")
        try fixture.item(
            "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa/cccccccc-cccc-4ccc-8ccc-cccccccccccc",
            "---\nschema: 1\ntitle: Second\norder: 2048\nicon: flag\nlabels: [bug, ui]\n---\nSecond body.\n"
        )
        try fixture.item(
            "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa/dddddddd-dddd-4ddd-8ddd-dddddddddddd",
            "---\nschema: 1\ntitle: First\norder: 1024\n---\nFirst body.\n"
        )
        try fixture.item(
            "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb/eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee",
            "---\nschema: 1\ntitle: Shipped\norder: 1024\n---\nShipped body.\n"
        )
        // The trash: one card and one lane, both of which a board print must not reach.
        try fixture.item(
            ".trash/ffffffff-ffff-4fff-8fff-ffffffffffff",
            "---\nschema: 1\nkind: card\ntitle: Deleted card\nmodified: 2026-03-03T09:00:00Z\n---\nGone.\n"
        )
        try fixture.item(
            ".trash/99999999-9999-4999-8999-999999999999",
            "---\nschema: 1\nkind: lane\ntitle: Deleted lane\nmodified: 2026-03-04T09:00:00Z\n---\n"
        )
        return fixture
    }

    @Test("Lanes and cards arrive in display order, and the trash is not reachable")
    func laneAndCardOrder() throws {
        let fixture = try self.fixture()
        defer { fixture.tearDown() }
        let snapshot = try BoardLoader.load(boardRoot: fixture.root).model

        // The fixture really does have a trash — otherwise the exclusion below proves nothing.
        #expect(snapshot.trash.count == 1)
        #expect(snapshot.trashedLanes.count == 1)

        let source = PrintSource.board(snapshot, titled: "Roadmap")
        #expect(source.scope == .board)
        #expect(source.lanes.map(\.title) == ["Doing", "Done"])
        #expect(source.lanes[0].cards.map(\.title) == ["First", "Second"], "by rank, not by folder name")
        #expect(source.lanes[1].cards.map(\.title) == ["Shipped"])

        let printed = PrintDocumentBuilder.blocks(from: source, options: PrintOptions())
        #expect(!printed.contains(.cardTitle("Deleted card")))
        #expect(!printed.contains(.laneHeading("Deleted lane")))
    }

    @Test("A card's icon, labels and body cross over; an unknown icon does not")
    func cardFields() throws {
        let fixture = try self.fixture()
        defer { fixture.tearDown() }
        let snapshot = try BoardLoader.load(boardRoot: fixture.root).model
        let source = PrintSource.board(snapshot, titled: "Roadmap")

        let second = try #require(source.lanes.first?.cards.last)
        #expect(second.title == "Second")
        #expect(second.icon == "flag")
        #expect(second.labels == ["bug", "ui"])
        #expect(second.body == "Second body.\n")

        let first = try #require(source.lanes.first?.cards.first)
        #expect(first.icon == nil, "a card with no chosen icon prints none — the level default stays on screen")
        #expect(first.labels.isEmpty)
    }

    @Test("A card print carries its lane and its board without printing headings for them")
    func cardScopeExtraction() throws {
        let fixture = try self.fixture()
        defer { fixture.tearDown() }
        let snapshot = try BoardLoader.load(boardRoot: fixture.root).model
        let lane = try #require(snapshot.lanes.first)
        let card = try #require(lane.cards.first)

        let source = PrintSource.card(card, laneTitle: lane.title.value, boardTitle: "Roadmap", comments: [])
        #expect(source.scope == .card)
        #expect(source.boardTitle == "Roadmap")
        #expect(source.lanes.map(\.title) == ["Doing"])
        #expect(PrintDocumentBuilder.blocks(from: source, options: PrintOptions()) == [
            .cardTitle("First"),
            .cardBody("First body.\n")
        ])
    }
}

// MARK: - The labels reading

@Suite("Print ▸ the reserved labels key")
struct PrintLabelsTests {

    private func labels(_ frontmatter: String) throws -> [String] {
        PrintCard.labels(of: try FrontmatterDocument.parse("---\nschema: 1\n\(frontmatter)---\nBody.\n"))
    }

    @Test("A sequence is its scalar members, in order")
    func sequence() throws {
        #expect(try labels("labels: [bug, ui, p1]\n") == ["bug", "ui", "p1"])
        #expect(try labels("labels:\n  - bug\n  - ui\n") == ["bug", "ui"])
    }

    @Test("A comma-separated scalar is the pair a human meant to type")
    func commaSeparatedScalar() throws {
        #expect(try labels("labels: bug, ui\n") == ["bug", "ui"])
        #expect(try labels("labels: bug\n") == ["bug"])
    }

    @Test("Non-string scalars read as the engine renders them")
    func nonStringScalars() throws {
        #expect(try labels("labels: [1, 2]\n") == ["1", "2"])
        #expect(try labels("labels: [true]\n") == ["true"])
    }

    @Test("Shapes that are not a label row contribute nothing, and never an error")
    func exoticShapes() throws {
        #expect(try labels("labels:\n") == [], "an explicit null")
        #expect(try labels("labels: ''\n") == [])
        #expect(try labels("labels: {a: 1}\n") == [], "a mapping is not a label row")
        #expect(try labels("labels: [[a, b], c]\n") == ["c"], "a nested list is skipped, not flattened")
        #expect(try labels("labels: [bug, '', ui]\n") == ["bug", "ui"], "blank members drop out")
        #expect(try labels("title: No labels here\n") == [], "an absent key")
    }
}

// MARK: - Header and footer

@Suite("Print ▸ the running head and foot")
struct PrintRunningHeadTests {

    @Test("Each toggle contributes exactly its own end")
    func toggles() {
        var options = PrintOptions()
        var header = PrintRunningHead.header(options: options, boardTitle: "Roadmap", dateText: "9 Aug 2026")
        #expect(header == PrintRunningHead.Line(leading: "Roadmap", trailing: "9 Aug 2026"))

        options.headerShowsBoardTitle = false
        header = PrintRunningHead.header(options: options, boardTitle: "Roadmap", dateText: "9 Aug 2026")
        #expect(header == PrintRunningHead.Line(leading: "", trailing: "9 Aug 2026"))

        options.headerShowsPrintDate = false
        header = PrintRunningHead.header(options: options, boardTitle: "Roadmap", dateText: "9 Aug 2026")
        #expect(header.isEmpty, "an empty line takes no paper at all")
    }

    @Test("The folio trails, the custom line leads")
    func footer() {
        var options = PrintOptions()
        options.footerShowsCustomLine = true
        options.footerCustomLine = "Confidential"

        #expect(PrintRunningHead.footer(options: options, pageText: "Page 2 of 7")
            == PrintRunningHead.Line(leading: "Confidential", trailing: "Page 2 of 7"))

        options.footerShowsPageNumbers = false
        #expect(PrintRunningHead.footer(options: options, pageText: "Page 2 of 7")
            == PrintRunningHead.Line(leading: "Confidential", trailing: ""))
    }

    @Test("A toggle left on over an emptied field prints nothing rather than an indent of air")
    func blankCustomLine() {
        var options = PrintOptions()
        options.footerShowsCustomLine = true
        options.footerCustomLine = "   "
        options.footerShowsPageNumbers = false
        #expect(PrintRunningHead.footer(options: options, pageText: "Page 1 of 1").isEmpty)
    }

    @Test("The folio's wording")
    func pageText() {
        #expect(PrintRunningHead.pageText(page: 3, of: 7) == "Page 3 of 7")
    }
}

// MARK: - The menu row

@Suite("Print ▸ menu validation")
struct PrintCommandValidationTests {

    /// **The row never disables now** (revised 2026-08-09) — a print is a read, so neither the
    /// read-only lock nor the focused-editor rule ever closed it, and the old third state
    /// ("nothing published, so grey out") turned out to leave the ⌘P chord live anyway, falling
    /// through to AppKit's own stock print handling (`PrintCommand`'s doc comment tells the whole
    /// story). `resolveScope` is what the row switches on instead: three answers, no `disabled` left
    /// to fall through.
    @Test("The board wins over a card, and neither published is a polite refusal — not a dead key")
    func resolution() {
        #expect(PrintCommand.resolveScope(hasBoard: true, hasPrintableCard: false) == .board)
        #expect(PrintCommand.resolveScope(hasBoard: false, hasPrintableCard: true) == .card)
        #expect(PrintCommand.resolveScope(hasBoard: true, hasPrintableCard: true) == .board, "the board in front wins")
        #expect(
            PrintCommand.resolveScope(hasBoard: false, hasPrintableCard: false) == .refuse,
            "welcome, or nothing at all — answered with a sentence, never silence"
        )
    }
}

// MARK: - Finder's half: the `printFiles` Apple Event

/// `PrintCoordinator.resolveFinderPrint(atPath:)` — the one part of the Finder-print path a test can
/// call without handing AppKit a real print job (`resolveFinderPrint`'s own doc comment). Everything
/// past this point (`printFiles`, `printHeadlessBoard`) drives a real `NSPrintOperation`, the same
/// AppKit boundary `PrintCoordinator.run`/`refuse` already sit past untested — this suite pins the
/// board-or-refuse *decision*, not what AppKit does with it.
@Suite("Print ▸ Finder printFiles resolution")
@MainActor
struct PrintFinderResolutionTests {

    @Test("A board's path resolves to its snapshot, extension-less exactly like every other board open")
    func resolvesABoard() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.board(title: "Roadmap")
        try fixture.lane(Ident.lane1, order: "1024", title: "Doing")
        try fixture.card(Ident.card1, in: Ident.lane1, order: "1024", title: "A card")

        // `fixture.root` itself carries no `.kanban` extension — the "both open" half of
        // `BoardModel`'s own doc comment, exercised by construction rather than by a second fixture.
        guard case let .board(model) = PrintCoordinator.resolveFinderPrint(atPath: fixture.root.path) else {
            Issue.record("expected the path to resolve as a board")
            return
        }
        #expect(model.title.value == "Roadmap")
        #expect(model.lanes.map(\.title.value) == ["Doing"])
        #expect(model.lanes.first?.cards.map(\.title.value) == ["A card"])
    }

    @Test("A folder with no root index.md refuses rather than throwing")
    func refusesANonBoard() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        // No `board()` call — an ordinary empty folder, the shape of "not a board at all".

        guard case .refuse = PrintCoordinator.resolveFinderPrint(atPath: fixture.root.path) else {
            Issue.record("expected the path to refuse")
            return
        }
    }

    @Test("A plain file refuses rather than throwing")
    func refusesAFile() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let fileURL = fixture.root.appendingPathComponent("not-a-board.txt")
        try Data("hello".utf8).write(to: fileURL)

        guard case .refuse = PrintCoordinator.resolveFinderPrint(atPath: fileURL.path) else {
            Issue.record("expected the path to refuse")
            return
        }
    }

    @Test("A board whose root schema is newer than this build refuses, the same fail-fast every open gives")
    func refusesAnUnsupportedSchema() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("", "---\nschema: 999\ntitle: Future\n---\n")

        guard case .refuse = PrintCoordinator.resolveFinderPrint(atPath: fixture.root.path) else {
            Issue.record("expected the path to refuse")
            return
        }
    }
}

// MARK: - Rendering and pagination, smoke-tested

@Suite("Print ▸ rendering and pagination")
@MainActor
struct PrintRenderingTests {

    private func session(_ source: PrintSource, options: PrintOptions) -> PrintSession {
        let (store, _, _) = makeStore()
        store.captureLastUsed(options)
        return PrintSession(
            provider: PrintSourceProvider(complete: source),
            profiles: store,
            cardFolder: nil,
            jobTitle: source.boardTitle,
            boardTitle: source.boardTitle
        )
    }

    private static func longBody(paragraphs: Int) -> String {
        (0 ..< paragraphs)
            .map { "Paragraph \($0). " + String(repeating: "Words that fill a line of a printed page. ", count: 6) }
            .joined(separator: "\n\n")
    }

    @Test("A page break really becomes a separately paginated section")
    func breaksSplitSections() {
        var options = PrintOptions()
        options.pageBreaks = .betweenLanes
        let source = board([
            PrintLane(title: "One", cards: [card("A", body: "a")]),
            PrintLane(title: "Two", cards: [card("B", body: "b")]),
            PrintLane(title: "Three", cards: [card("C", body: "c")])
        ])
        let blocks = PrintDocumentBuilder.blocks(from: source, options: options)
        #expect(PrintDocumentRenderer.sections(for: blocks, options: options).count == 3)

        // …and continuously, one section.
        var flowing = options
        flowing.pageBreaks = .flow
        let flowingBlocks = PrintDocumentBuilder.blocks(from: source, options: flowing)
        #expect(PrintDocumentRenderer.sections(for: flowingBlocks, options: flowing).count == 1)
    }

    @Test("An empty document renders no sections")
    func emptyDocument() {
        #expect(PrintDocumentRenderer.sections(for: [], options: PrintOptions()).isEmpty)
    }

    @Test("Between-lanes really costs a sheet per lane")
    func pageCountPerLane() {
        var options = PrintOptions()
        options.pageBreaks = .betweenLanes
        let source = board((1 ... 4).map { PrintLane(title: "Lane \($0)", cards: [card("Card \($0)", body: "words")]) })

        let view = PrintDocumentView(session: session(source, options: options), printInfo: NSPrintInfo())
        #expect(view.pageCount() == 4, "four short lanes, four sheets — the break is pagination, not spacing")
    }

    @Test("A long body paginates rather than clipping")
    func longBodyPaginates() {
        let options = PrintOptions()
        let short = board([PrintLane(title: "One", cards: [card("A", body: "one line")])])
        let long = board([PrintLane(title: "One", cards: [card("A", body: Self.longBody(paragraphs: 80))])])

        #expect(PrintDocumentView(session: session(short, options: options), printInfo: NSPrintInfo()).pageCount() == 1)
        let pages = PrintDocumentView(session: session(long, options: options), printInfo: NSPrintInfo()).pageCount()
        #expect(pages > 1, "eighty paragraphs do not fit on one sheet")
    }

    @Test("A byline reads as a sentence in all four states")
    func bylines() {
        #expect(PrintDocumentRenderer.byline(author: "ada", created: nil) == "ada")
        #expect(PrintDocumentRenderer.byline(author: nil, created: nil) == "Comment")
        #expect(PrintDocumentRenderer.byline(author: "  ", created: nil) == "Comment", "a blank author is no author")
        let dated = PrintDocumentRenderer.byline(author: "ada", created: stamp(0))
        #expect(dated.hasPrefix("ada — "))
        #expect(PrintDocumentRenderer.byline(author: nil, created: stamp(0)) == String(dated.dropFirst("ada — ".count)))
    }

    @Test("The thread's heading counts, and says 'comment' once")
    func commentsHeading() {
        #expect(PrintDocumentRenderer.commentsHeadingText(count: 1) == "1 comment")
        #expect(PrintDocumentRenderer.commentsHeadingText(count: 3) == "3 comments")
        #expect(PrintDocumentRenderer.commentsHeadingText(count: 0) == "0 comments")
    }

    @Test("The chosen face reaches the text, and code keeps its own")
    func faceRemap() {
        // Courier is on every Mac; asserting a family that might not be installed would be asserting a
        // fixture about the machine.
        let family = "Times New Roman"
        guard PrintTypography.families().contains(family) else { return }

        var options = PrintOptions()
        options.fontFamily = family
        let blocks: [PrintBlock] = [.cardBody("Words, and `code`.")]
        let section = try? #require(PrintDocumentRenderer.sections(for: blocks, options: options).first)
        guard let section else { return }

        var sawFace = false
        var sawMono = false
        section.enumerateAttribute(.font, in: NSRange(location: 0, length: section.length)) { value, _, _ in
            guard let font = value as? NSFont else { return }
            if font.fontDescriptor.symbolicTraits.contains(.monoSpace) {
                sawMono = true
            } else if font.familyName == family {
                sawFace = true
            }
        }
        #expect(sawFace, "the body is set in the chosen family")
        #expect(sawMono, "inline code stays monospaced — a fenced block in Palatino is nobody's intent")
    }

    @Test("A summary line describes what the panel is about to print")
    func summary() {
        var options = PrintOptions()
        #expect(PrintOptionsSummary.includes(options) == "Title, labels, body")
        #expect(PrintOptionsSummary.pageBreaks(options) == "Continuous")
        #expect(PrintOptionsSummary.type(options) == "System 11 pt")

        options.includesComments = true
        options.commentSort = .newestFirst
        options.fontFamily = "Palatino"
        options.fontSize = 13.5
        options.pageBreaks = .betweenCards
        #expect(PrintOptionsSummary.includes(options) == "Title, labels, body, comments (newest first)")
        #expect(PrintOptionsSummary.pageBreaks(options) == "Between cards")
        #expect(PrintOptionsSummary.type(options) == "Palatino 13.5 pt")

        options.includesTitle = false
        options.includesLabels = false
        options.includesBody = false
        options.includesComments = false
        #expect(PrintOptionsSummary.includes(options) == "Nothing")
    }
}

// MARK: - The session

@Suite("Print ▸ the panel's session")
@MainActor
struct PrintSessionTests {

    private func session(profiles: PrintProfileStore) -> PrintSession {
        PrintSession(
            provider: PrintSourceProvider(complete: board([PrintLane(title: "One", cards: [card("A", body: "a")])])),
            profiles: profiles,
            cardFolder: nil,
            jobTitle: "Roadmap",
            boardTitle: "Roadmap"
        )
    }

    @Test("A session opens on Last Used, which is the defaults on a first print")
    func opensOnLastUsed() {
        let (store, _, teardown) = makeStore()
        defer { teardown() }

        #expect(session(profiles: store).options == PrintOptions())

        store.captureLastUsed(exoticOptions())
        let second = session(profiles: store)
        #expect(second.selectedProfileName == PrintProfile.lastUsedName)
        #expect(second.options == exoticOptions())
    }

    @Test("Choosing a profile copies its options in; editing them afterwards does not write back")
    func selectingIsACopy() {
        let (store, _, teardown) = makeStore()
        defer { teardown() }
        store.save(exoticOptions(), as: "Archive")

        let session = self.session(profiles: store)
        session.selectProfile(named: "Archive")
        #expect(session.options == exoticOptions())
        #expect(!session.isModified)

        session.options.fontSize = 9
        #expect(session.isModified, "the popup says modified")
        #expect(store.options(named: "Archive")?.fontSize == exoticOptions().fontSize, "the profile is untouched")

        #expect(session.saveProfile(named: "Archive"))
        #expect(store.options(named: "Archive")?.fontSize == 9)
        #expect(!session.isModified)
    }

    @Test("The reserved row is never 'modified' — drifting from it is what it is for")
    func reservedRowIsNeverModified() {
        let (store, _, teardown) = makeStore()
        defer { teardown() }

        let session = self.session(profiles: store)
        session.options.fontSize = 30
        #expect(!session.isModified)
        #expect(!session.canManageSelection, "the reserved row cannot be renamed or deleted")
    }

    @Test("Deleting a profile keeps the settings in front of the user")
    func deleteKeepsOptions() {
        let (store, _, teardown) = makeStore()
        defer { teardown() }
        store.save(exoticOptions(), as: "Archive")

        let session = self.session(profiles: store)
        session.selectProfile(named: "Archive")
        session.deleteSelectedProfile()

        #expect(store.catalog.names.isEmpty)
        #expect(session.selectedProfileName == PrintProfile.lastUsedName)
        #expect(session.options == exoticOptions(), "a management gesture is not a reset")
    }

    @Test("Selecting a name that resolves to nothing changes nothing")
    func selectingAGhost() {
        let (store, _, teardown) = makeStore()
        defer { teardown() }

        let session = self.session(profiles: store)
        session.options.fontSize = 20
        session.selectProfile(named: "Deleted elsewhere")
        #expect(session.selectedProfileName == PrintProfile.lastUsedName)
        #expect(session.options.fontSize == 20)
    }

    @Test("The comment-bearing source is read only when the options ask for it")
    func commentsAreReadLazily() {
        let (store, _, teardown) = makeStore()
        defer { teardown() }

        var reads = 0
        let withComments = board([PrintLane(title: "One", cards: [
            card("A", body: "a", comments: [PrintComment(author: "ada", created: stamp(0), body: "hi")])
        ])])
        let provider = PrintSourceProvider(
            withoutComments: board([PrintLane(title: "One", cards: [card("A", body: "a")])]),
            withComments: {
                reads += 1
                return withComments
            }
        )
        let session = PrintSession(
            provider: provider,
            profiles: store,
            cardFolder: nil,
            jobTitle: "Roadmap",
            boardTitle: "Roadmap"
        )

        _ = session.blocks()
        _ = session.blocks()
        #expect(reads == 0, "comments are off by default, so no thread is read")

        session.options.includesComments = true
        _ = session.blocks()
        _ = session.blocks()
        #expect(reads == 1, "and once the user asks, exactly one read serves every relayout")
    }
}

// MARK: - Symbols on paper

/// **The printed-symbol regression** (owner report, 2026-08-08: "SF symbols don't render well in the PDF
/// output of File ▸ Print…", with a screenshot of solid dark rectangles where the card icons belong).
///
/// The typography around it is still legitimately untested — see this file's own note — but this failure is
/// not typography. It is a drawing fact with two halves, and both are assertable:
///
/// - an `NSImage(systemSymbolName:)` is a **template**, and a print/PDF context tints one across its whole
///   box rather than through its coverage, which is the black rectangle the owner photographed;
/// - a PDF context is a 1× device, so even a non-template symbol image lands as a 13-pixel bitmap and
///   blurs at any zoom.
///
/// So the suite asserts what `PrintSymbol` hands over (concrete artwork, at paper resolution, on the
/// symbol's own baseline) and then prints a page whose only ink is one icon and measures it: a glyph
/// covers a fraction of its own bounding box, a template box covers all of it. The second test would have
/// failed on the shipped build at ~1.0 coverage, which is the whole point of writing it that way.
@Suite("Print ▸ symbols on paper")
@MainActor
struct PrintSymbolTests {

    /// A symbol every macOS has had for years, and one whose artwork is plainly not a rectangle.
    private static let symbol = "lightbulb"

    // MARK: The image handed to the page

    @Test("A printed symbol is concrete artwork, not a template to be tinted by machinery that is not there")
    func artworkNotTemplate() throws {
        let rendered = try #require(PrintSymbol.rendered(Self.symbol, pointSize: 11, ink: PrintTypography.secondaryInk))

        #expect(!rendered.image.isTemplate, "a template image is what drew the black boxes")
        let rep = try #require(rendered.image.representations.first as? NSBitmapImageRep)
        #expect(rep.size == rendered.image.size, "the extra pixels are resolution, not a bigger picture")
        #expect(
            CGFloat(rep.pixelsWide) >= rep.size.width * 4,
            "a page wants far more than the 72 ppi a PDF context would rasterize at"
        )
    }

    @Test("The artwork is a glyph — most of its box is paper")
    func artworkIsNotFilled() throws {
        let rendered = try #require(PrintSymbol.rendered(Self.symbol, pointSize: 11, ink: PrintTypography.secondaryInk))
        let rep = try #require(rendered.image.representations.first as? NSBitmapImageRep)
        let bytes = try #require(rep.bitmapData)

        var inked = 0
        for y in 0 ..< rep.pixelsHigh {
            for x in 0 ..< rep.pixelsWide {
                let alpha = bytes[y * rep.bytesPerRow + x * rep.samplesPerPixel + 3]
                if alpha > 12 { inked += 1 }
            }
        }
        let coverage = Double(inked) / Double(rep.pixelsWide * rep.pixelsHigh)
        #expect(coverage > 0.02, "a symbol that drew nothing at all is the other way to fail")
        #expect(coverage < 0.75, "a filled box is the template bug; a lightbulb is an outline")
    }

    @Test("An unknown name draws nothing rather than a box — `ItemSymbol`'s promise, kept on paper")
    func unknownNameIsOmitted() {
        #expect(PrintSymbol.rendered("not.a.symbol.anybody.ships", pointSize: 11, ink: .black) == nil)
    }

    // MARK: The baseline

    @Test("The offset is the symbol's own baseline, per symbol and scaling with the size")
    func baselineComesFromTheSymbol() throws {
        let ink = PrintTypography.secondaryInk
        let bulb = try #require(PrintSymbol.rendered(Self.symbol, pointSize: 11, ink: ink))
        let text = try #require(PrintSymbol.rendered("textformat", pointSize: 11, ink: ink))
        let larger = try #require(PrintSymbol.rendered(Self.symbol, pointSize: 22, ink: ink))

        // Below the baseline, always — the old constant was `font.descender * 0.5`, which knew nothing
        // about which symbol it was placing.
        #expect(bulb.baselineOffset < 0)
        #expect(
            bulb.baselineOffset < text.baselineOffset,
            "a lightbulb's base sits under the baseline; `textformat` sits on it"
        )
        #expect(larger.baselineOffset < bulb.baselineOffset, "and the offset is a length, so it scales")

        let source = try #require(NSImage(systemSymbolName: Self.symbol, accessibilityDescription: nil)?
            .withSymbolConfiguration(NSImage.SymbolConfiguration(pointSize: 11, weight: .regular)))
        #expect(bulb.baselineOffset == -source.alignmentRect.origin.y, "which is Apple's own metric, not a guess")
    }

    // MARK: The renderer's line

    @Test("The icon reaches the page as an attachment carrying that artwork")
    func theLineCarriesTheArtwork() throws {
        let blocks: [PrintBlock] = [.cardMeta(icon: Self.symbol, labels: ["idea"])]
        let section = try #require(PrintDocumentRenderer.sections(for: blocks, options: PrintOptions()).first)

        var found: NSImage?
        section.enumerateAttribute(.attachment, in: NSRange(location: 0, length: section.length)) { value, _, _ in
            if let attachment = value as? NSTextAttachment { found = attachment.image }
        }
        let image = try #require(found, "the icon is a text attachment on the labels line")
        #expect(!image.isTemplate)
        #expect(section.string.contains("idea"), "and the labels are still on the line beside it")
    }

    @Test("A symbol this system cannot draw still prints its labels")
    func aMissingSymbolKeepsTheLabels() throws {
        let blocks: [PrintBlock] = [.cardMeta(icon: "not.a.symbol.anybody.ships", labels: ["idea"])]
        let section = try #require(PrintDocumentRenderer.sections(for: blocks, options: PrintOptions()).first)
        #expect(section.string.contains("idea"))
    }

    // MARK: What actually lands on paper

    /// A page whose only ink is one icon: a card print, every component but the labels line switched off,
    /// a card with an icon and no labels, and no running head or foot. Whatever is dark on that sheet is
    /// the symbol and nothing else, so it can be measured rather than eyeballed.
    private func iconOnlyOptions() -> PrintOptions {
        var options = PrintOptions()
        options.includesTitle = false
        options.includesBody = false
        options.includesComments = false
        options.includesLabels = true
        options.headerShowsBoardTitle = false
        options.headerShowsPrintDate = false
        options.footerShowsPageNumbers = false
        options.footerShowsCustomLine = false
        return options
    }

    private func iconOnlyPDF(icon: String) -> Data {
        let (store, _, teardown) = makeStore()
        defer { teardown() }
        let options = iconOnlyOptions()
        store.captureLastUsed(options)

        let source = PrintSource(
            scope: .card,
            boardTitle: "Roadmap",
            lanes: [PrintLane(title: "One", cards: [PrintCard(title: "A", icon: icon)])]
        )
        let session = PrintSession(
            provider: PrintSourceProvider(complete: source),
            profiles: store,
            cardFolder: nil,
            jobTitle: "A",
            boardTitle: "Roadmap"
        )

        let info = NSPrintInfo()
        let view = PrintDocumentView(session: session, printInfo: info)
        _ = view.pageCount()

        let data = NSMutableData()
        let operation = NSPrintOperation.pdfOperation(with: view, inside: view.bounds, to: data, printInfo: info)
        operation.showsPrintPanel = false
        operation.showsProgressPanel = false
        operation.run()
        return data as Data
    }

    /// The document's first page, rasterized onto white at `scale` times its point size — what a reader
    /// with a magnifying glass would see.
    private func firstPage(of pdf: Data, scale: CGFloat) -> NSBitmapImageRep? {
        guard let page = NSPDFImageRep(data: pdf) else { return nil }
        page.currentPage = 0
        let box = page.size
        guard let rep = NSBitmapImageRep(
            bitmapDataPlanes: nil,
            pixelsWide: Int(box.width * scale), pixelsHigh: Int(box.height * scale),
            bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false,
            colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0
        ) else { return nil }
        rep.size = box

        NSGraphicsContext.saveGraphicsState()
        NSGraphicsContext.current = NSGraphicsContext(bitmapImageRep: rep)
        NSColor.white.setFill()
        CGRect(origin: .zero, size: box).fill()
        page.draw(in: CGRect(origin: .zero, size: box))
        NSGraphicsContext.restoreGraphicsState()
        return rep
    }

    /// The bounding box of the ink on a page, and how much of that box the ink fills — in points, and as a
    /// fraction. A glyph fills a fraction of its box; the template bug filled all of it.
    private func ink(on rep: NSBitmapImageRep, scale: CGFloat) -> (box: CGRect, coverage: Double)? {
        guard let bytes = rep.bitmapData else { return nil }
        var minX = rep.pixelsWide, maxX = -1, minY = rep.pixelsHigh, maxY = -1
        var inked = 0
        for y in 0 ..< rep.pixelsHigh {
            let row = y * rep.bytesPerRow
            for x in 0 ..< rep.pixelsWide where bytes[row + x * rep.samplesPerPixel] < 220 {
                inked += 1
                minX = min(minX, x); maxX = max(maxX, x)
                minY = min(minY, y); maxY = max(maxY, y)
            }
        }
        guard maxX >= minX, maxY >= minY else { return nil }
        let width = maxX - minX + 1
        let height = maxY - minY + 1
        return (
            CGRect(x: CGFloat(minX) / scale, y: CGFloat(minY) / scale,
                   width: CGFloat(width) / scale, height: CGFloat(height) / scale),
            Double(inked) / Double(width * height)
        )
    }

    @Test("A printed page draws the symbol as a glyph, at the size the type scale asked for")
    func thePageDrawsAGlyph() throws {
        let scale: CGFloat = 4
        let pdf = iconOnlyPDF(icon: Self.symbol)
        #expect(!pdf.isEmpty, "the operation produced a document")

        let page = try #require(firstPage(of: pdf, scale: scale))
        let measured = try #require(ink(on: page, scale: scale), "the icon is the only ink on the sheet")

        // The line's own size, which is what the icon is built at.
        let expected = try #require(PrintSymbol.rendered(
            Self.symbol,
            pointSize: PrintTypography.secondary(iconOnlyOptions()).pointSize,
            ink: PrintTypography.secondaryInk
        )).image.size

        #expect(
            measured.coverage < 0.75,
            "coverage \(measured.coverage) — a filled box is the template bug the owner photographed"
        )
        #expect(measured.coverage > 0.05, "and something was drawn")

        // The ink sits *inside* the symbol's box and fills most of it: a lightbulb is narrower than the
        // box it is drawn in, but an icon at the wrong size — the whole page, or a stray point size —
        // would miss either bound by a mile.
        #expect(
            measured.box.width <= expected.width + 1 && measured.box.height <= expected.height + 1,
            "ink measured \(measured.box.size) against a symbol of \(expected)"
        )
        #expect(
            measured.box.width >= expected.width * 0.5 && measured.box.height >= expected.height * 0.5,
            "ink measured \(measured.box.size) against a symbol of \(expected)"
        )
    }
}
