import AppKit
import CoreGraphics
import Foundation
import ImageIO
import Testing
import UniformTypeIdentifiers
@testable import Kanban

/// **Pasting a picture** (04-interactions.md ▸ Clipboard's image-data branch, ruled 2026-08-09) —
/// the pasteboard classification and its precedence, the format rule, the Finder-style name and its
/// ladder, the hero key's write, and the three surfaces' validation.
///
/// Like every other write suite here the landing tests drive a **real store over a real temp board**
/// and read back through the loader or the raw bytes, never through a snapshot the store handed out.
/// `WriterFixture`, `Ident` and `Item` come from `WriterTestSupport.swift`; `FakePasteboard`,
/// `ClipboardHarness` and the board fixture come from `ClipboardTests.swift`.

// MARK: - Making pictures

/// A tiny opaque bitmap, encoded under `type` — real bytes, because the format rule's whole claim is
/// about what ImageIO can and cannot read, and a `Data("png".utf8)` stand-in would make the
/// conversion tests vacuous.
func encodedImage(_ type: UTType, side: Int = 4) -> Data {
    let space = CGColorSpaceCreateDeviceRGB()
    let context = CGContext(
        data: nil,
        width: side,
        height: side,
        bitsPerComponent: 8,
        bytesPerRow: 0,
        space: space,
        bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
    )!
    context.setFillColor(CGColor(red: 0.2, green: 0.6, blue: 0.9, alpha: 1))
    context.fill(CGRect(x: 0, y: 0, width: side, height: side))
    let image = context.makeImage()!

    let output = NSMutableData()
    let destination = CGImageDestinationCreateWithData(output, type.identifier as CFString, 1, nil)!
    CGImageDestinationAddImage(destination, image, nil)
    _ = CGImageDestinationFinalize(destination)
    return output as Data
}

/// The type identifier of whatever `data` actually is, as ImageIO reads it — the only honest way to
/// assert "the bytes on disk are still a PNG".
func imageType(of data: Data) -> String? {
    guard let source = CGImageSourceCreateWithData(data as CFData, nil) else { return nil }
    return CGImageSourceGetType(source) as String?
}

/// One app-mediated reload landed — `GeneratedBackgroundTests`' own helper, because the echo-window
/// tests here are about exactly the same memo and have to open and close that window the same way.
@MainActor
private func settle(_ store: BoardStore) async {
    store.handleWatcherEvent(.treeChanged(.appMediated))
    await store.awaitQuiescence()
}

// MARK: - Classification

@Suite("PastedImage ▸ classification")
struct PastedImageClassificationTests {

    @Test("The app's own clipboard type wins outright — a picture beside it never diverts ⌘V")
    func boardItemsWin() {
        let flavor = PastedImage.flavor(
            hasBoardItems: true,
            types: [UTType.laneworkClipboard.identifier, UTType.png.identifier, UTType.tiff.identifier]
        )
        #expect(flavor == nil)
    }

    /// The ruling's own parenthesis: "IMAGE DATA (no file URL)". A Finder copy of a PNG puts a file
    /// URL down, often with an image flavor beside it, and that is a different gesture's payload.
    @Test("A file URL suppresses the branch, whatever else rides beside it")
    func fileURLsSuppress() {
        #expect(PastedImage.flavor(
            hasBoardItems: false,
            types: [UTType.fileURL.identifier, UTType.png.identifier]
        ) == nil)
        // Conformance, not equality: a subtype of `public.file-url` is still a file reference.
        #expect(PastedImage.carriesFileURL([UTType.fileURL.identifier]))
        #expect(!PastedImage.carriesFileURL([UTType.png.identifier, UTType.tiff.identifier]))
        // A type the system does not know is not a file URL — the optimistic reading a drag's
        // unknown types get.
        #expect(!PastedImage.carriesFileURL(["com.example.nothing-at-all"]))
    }

    @Test("Raw image data is the fallback, and an empty pasteboard offers nothing")
    func imageDataIsTheFallback() throws {
        let flavor = try #require(PastedImage.flavor(hasBoardItems: false, types: [UTType.png.identifier]))
        #expect(flavor.type == UTType.png.identifier)
        #expect(PastedImage.flavor(hasBoardItems: false, types: []) == nil)
        #expect(PastedImage.flavor(hasBoardItems: false, types: [UTType.plainText.identifier]) == nil)
    }

    /// The screenshot's exact pasteboard: PNG and TIFF together. Our order wins over the
    /// pasteboard's, so the common paste costs no decode and no re-encode at all.
    @Test("PNG beats TIFF however the pasteboard orders them")
    func pngBeatsTIFF() throws {
        for types in [
            [UTType.tiff.identifier, UTType.png.identifier],
            [UTType.png.identifier, UTType.tiff.identifier],
        ] {
            let flavor = try #require(PastedImage.flavor(hasBoardItems: false, types: types))
            #expect(flavor.type == UTType.png.identifier)
            #expect(!flavor.convertsToPNG)
            #expect(flavor.fileName == "Pasted Image.png")
        }
    }

    @Test("A file-shaped flavor keeps its own extension; TIFF and BMP become PNG")
    func theFormatRule() throws {
        let jpeg = try #require(PastedImage.flavor(hasBoardItems: false, types: [UTType.jpeg.identifier]))
        #expect(!jpeg.convertsToPNG)
        #expect(jpeg.fileExtension == UTType.jpeg.preferredFilenameExtension)

        let gif = try #require(PastedImage.flavor(hasBoardItems: false, types: [UTType.gif.identifier]))
        #expect(!gif.convertsToPNG, "a GIF's animation does not survive a single-frame decode")
        #expect(gif.fileName == "Pasted Image.gif")

        for interchange in [UTType.tiff, UTType.bmp] {
            let flavor = try #require(
                PastedImage.flavor(hasBoardItems: false, types: [interchange.identifier])
            )
            #expect(flavor.convertsToPNG)
            #expect(flavor.fileExtension == "png")
            #expect(flavor.type == interchange.identifier, "the bytes still come from the offered type")
        }
    }

    @Test("The two names are the two folders' — a card's attachment and a board's backdrop")
    func theTwoNames() throws {
        let flavor = try #require(PastedImage.flavor(hasBoardItems: false, types: [UTType.jpeg.identifier]))
        #expect(flavor.fileName == "Pasted Image.jpeg")
        #expect(flavor.backgroundFileName == "Pasted Background.jpeg")
    }
}

// MARK: - The bytes

@Suite("PastedImage ▸ encoding")
struct PastedImageEncodingTests {

    @Test("A verbatim flavor is handed back byte for byte — no decode, no re-encode")
    func verbatimIsUntouched() throws {
        let png = encodedImage(.png)
        let flavor = try #require(PastedImage.flavor(hasBoardItems: false, types: [UTType.png.identifier]))
        #expect(PastedImage.encode(png, as: flavor) == png)
    }

    @Test("TIFF is re-encoded, and what comes out really is a PNG")
    func tiffBecomesPNG() throws {
        let tiff = encodedImage(.tiff)
        let flavor = try #require(PastedImage.flavor(hasBoardItems: false, types: [UTType.tiff.identifier]))
        let encoded = try #require(PastedImage.encode(tiff, as: flavor))
        #expect(encoded != tiff)
        #expect(imageType(of: encoded) == UTType.png.identifier)
    }

    /// A pasteboard that declares a flavor it cannot back up. Nothing is written, which is the honest
    /// outcome — see `ClipboardStore.pasteImage(intoCard:in:)`.
    @Test("Bytes that are not an image encode to nothing rather than to a broken file")
    func undecodableBytesRefuse() throws {
        let tiff = try #require(PastedImage.flavor(hasBoardItems: false, types: [UTType.tiff.identifier]))
        #expect(PastedImage.encode(Data("not a picture".utf8), as: tiff) == nil)

        let png = try #require(PastedImage.flavor(hasBoardItems: false, types: [UTType.png.identifier]))
        #expect(PastedImage.encode(Data(), as: png) == nil, "an empty payload is not a file")
    }
}

// MARK: - Which attachments can be a hero

@Suite("PastedImage ▸ image names")
struct PastedImageNameTests {

    @Test("An image is decided by extension, and anything else is not one")
    func imageNames() {
        #expect(PastedImage.isImageName("Pasted Image.png"))
        #expect(PastedImage.isImageName("photo.JPEG"), "extensions are case-insensitive")
        #expect(PastedImage.isImageName("clip.gif"))
        #expect(!PastedImage.isImageName("notes.txt"))
        #expect(!PastedImage.isImageName("archive.zip"))
        #expect(!PastedImage.isImageName("README"), "no extension, no reading")
        #expect(!PastedImage.isImageName(""))
    }
}

// MARK: - Where a picture lands on the board

@MainActor
@Suite("PasteTarget ▸ the image branch")
struct PasteImageTargetTests {

    private func snapshot() throws -> BoardModel {
        let fixture = try makeClipboardBoard()
        defer { fixture.tearDown() }
        return try BoardLoader.load(boardRoot: fixture.root).model
    }

    @Test("The anchor card, which is the last selected in flatten order")
    func theAnchorCard() throws {
        let model = try snapshot()
        #expect(PasteTarget.card(
            selection: ItemReferenceSet(ids: [clipboardCard1], container: .board),
            snapshot: model
        ) == clipboardCard1)
        // Two selected: the last in flatten order, the shared anchor rule.
        #expect(PasteTarget.card(
            selection: ItemReferenceSet(ids: [clipboardCard1, clipboardCard4], container: .board),
            snapshot: model
        ) == clipboardCard4)
    }

    /// A picture has to land in *some* card's `attachments/`, and there is no card the app could pick
    /// without inventing one — so these three all answer nothing and the menu greys out.
    @Test("A lane, an empty and a trash selection all anchor no card")
    func nothingToAnchorOn() throws {
        let model = try snapshot()
        #expect(PasteTarget.card(
            selection: ItemReferenceSet(ids: [clipboardLane1], container: .board),
            snapshot: model
        ) == nil)
        #expect(PasteTarget.card(selection: .empty, snapshot: model) == nil)
        #expect(PasteTarget.card(
            selection: ItemReferenceSet(ids: [clipboardCard3], container: .trash),
            snapshot: model
        ) == nil)
    }
}

// MARK: - The paste itself

@MainActor
@Suite("Paste ▸ an image into a card")
struct PasteImageWriteTests {

    /// Attachment names as the loader sees them — never the store's snapshot, which a write
    /// deliberately does not touch (the one-way flow).
    private func attachments(_ card: String, in fixture: WriterFixture) throws -> [String] {
        let model = try BoardLoader.load(boardRoot: fixture.root).model
        for lane in model.lanes {
            if let match = lane.cards.first(where: { $0.id.rawValue == card }) { return match.attachments }
        }
        return []
    }

    @Test("A screenshot lands as 'Pasted Image.png', byte for byte, in the anchor card")
    func aScreenshotLands() throws {
        let harness = try makeClipboardHarness()
        defer { harness.tearDown() }
        let png = encodedImage(.png)
        harness.pasteboard.seed([
            (UTType.png.identifier, png),
            (UTType.tiff.identifier, encodedImage(.tiff)),
        ])
        harness.store.select([clipboardCard2], in: .board)

        #expect(harness.clipboard.pasteImage(into: harness.store))

        #expect(try attachments(Ident.card2, in: harness.fixture) == ["Pasted Image.png"])
        #expect(try harness.fixture.data("\(Ident.lane1)/\(Ident.card2)/attachments/Pasted Image.png") == png)
        #expect(harness.store.banners.oneShots.isEmpty)
    }

    @Test("A second paste climbs the Finder ladder rather than overwriting")
    func theFinderLadder() throws {
        let harness = try makeClipboardHarness()
        defer { harness.tearDown() }
        harness.pasteboard.seed([(UTType.png.identifier, encodedImage(.png))])
        harness.store.select([clipboardCard2], in: .board)

        #expect(harness.clipboard.pasteImage(into: harness.store))
        #expect(harness.clipboard.pasteImage(into: harness.store))
        #expect(harness.clipboard.pasteImage(into: harness.store))

        #expect(try attachments(Ident.card2, in: harness.fixture)
            == ["Pasted Image 2.png", "Pasted Image 3.png", "Pasted Image.png"])
    }

    /// The name is minted against what is on disk, so a hand-placed file of the same name is the
    /// user's and is never written through — `BoardWriter.freshName`'s rule, inherited whole.
    @Test("A file already holding the name is stepped around, never overwritten")
    func anExistingNameIsRespected() throws {
        let harness = try makeClipboardHarness()
        defer { harness.tearDown() }
        let mine = Data("hand placed".utf8)
        try harness.fixture.file("\(Ident.lane1)/\(Ident.card2)/attachments/Pasted Image.png", mine)
        harness.pasteboard.seed([(UTType.png.identifier, encodedImage(.png))])
        harness.store.select([clipboardCard2], in: .board)

        #expect(harness.clipboard.pasteImage(into: harness.store))

        #expect(try harness.fixture.data("\(Ident.lane1)/\(Ident.card2)/attachments/Pasted Image.png") == mine)
        #expect(try attachments(Ident.card2, in: harness.fixture) == ["Pasted Image 2.png", "Pasted Image.png"])
    }

    @Test("A TIFF-only pasteboard lands a PNG")
    func tiffLandsAsPNG() throws {
        let harness = try makeClipboardHarness()
        defer { harness.tearDown() }
        harness.pasteboard.seed([(UTType.tiff.identifier, encodedImage(.tiff))])
        harness.store.select([clipboardCard2], in: .board)

        #expect(harness.clipboard.pasteImage(into: harness.store))

        #expect(try attachments(Ident.card2, in: harness.fixture) == ["Pasted Image.png"])
        let landed = try harness.fixture.data("\(Ident.lane1)/\(Ident.card2)/attachments/Pasted Image.png")
        #expect(imageType(of: landed) == UTType.png.identifier)
    }

    @Test("A pasteboard that declared a flavor it cannot back up writes nothing at all")
    func aLyingPasteboardWritesNothing() throws {
        let harness = try makeClipboardHarness()
        defer { harness.tearDown() }
        harness.pasteboard.seed([(UTType.tiff.identifier, Data("not a picture".utf8))])
        harness.store.select([clipboardCard2], in: .board)

        #expect(!harness.clipboard.pasteImage(into: harness.store))
        #expect(try attachments(Ident.card2, in: harness.fixture).isEmpty)
        #expect(harness.store.banners.oneShots.isEmpty)
    }

    /// The card window's branch: the target is the window's own card, whatever the board's selection
    /// happens to be.
    @Test("The card window pastes onto its own card, not onto the board's selection")
    func theCardWindowsOwnCard() throws {
        let harness = try makeClipboardHarness()
        defer { harness.tearDown() }
        harness.pasteboard.seed([(UTType.png.identifier, encodedImage(.png))])
        harness.store.select([clipboardCard2], in: .board)

        #expect(harness.clipboard.pasteImage(intoCard: clipboardCard4, in: harness.store))

        #expect(try attachments(Ident.card4, in: harness.fixture) == ["Pasted Image.png"])
        #expect(try attachments(Ident.card2, in: harness.fixture).isEmpty)
    }

    @Test("A board payload on the pasteboard is never diverted into an attachment")
    func aBoardPayloadStillPastesAsCards() throws {
        let harness = try makeClipboardHarness()
        defer { harness.tearDown() }
        harness.store.select([clipboardCard1], in: .board)
        harness.clipboard.copy(from: harness.store)

        #expect(harness.clipboard.imagePayload == nil)
        #expect(!harness.clipboard.canPasteImage(into: harness.store))
        #expect(harness.clipboard.canPaste(into: harness.store))
    }
}

// MARK: - Validation

@MainActor
@Suite("Paste ▸ image menu validation")
struct PasteImageValidationTests {

    @Test("The board branch needs a picture and a card to put it on")
    func theBoardBranchsClauses() throws {
        let harness = try makeClipboardHarness()
        defer { harness.tearDown() }

        harness.store.select([clipboardCard1], in: .board)
        #expect(!harness.clipboard.canPasteImage(into: harness.store), "nothing on the pasteboard")

        harness.pasteboard.seed([(UTType.png.identifier, encodedImage(.png))])
        harness.clipboard.refresh()
        #expect(harness.clipboard.canPasteImage(into: harness.store))

        // A lane anchors no card, so the row greys out — and so does the paste.
        harness.store.select([clipboardLane1], in: .board)
        #expect(!harness.clipboard.canPasteImage(into: harness.store))
        #expect(!harness.clipboard.pasteImage(into: harness.store))

        harness.store.clearSelection()
        #expect(!harness.clipboard.canPasteImage(into: harness.store))
    }

    @Test("A file URL on the pasteboard offers nothing to any of the three surfaces")
    func fileURLsOfferNothing() throws {
        let harness = try makeClipboardHarness()
        defer { harness.tearDown() }
        harness.pasteboard.seed([
            (UTType.fileURL.identifier, Data("file:///tmp/shot.png".utf8)),
            (UTType.png.identifier, encodedImage(.png)),
        ])
        harness.clipboard.refresh()
        harness.store.select([clipboardCard1], in: .board)

        #expect(harness.clipboard.imagePayload == nil)
        #expect(!harness.clipboard.canPasteImage(into: harness.store))
        #expect(!harness.clipboard.canPasteImage(intoCard: clipboardCard1, in: harness.store))
        #expect(!harness.clipboard.canPasteBoardBackground(into: harness.store))
    }

    /// The board's backdrop has no target to resolve, which is what makes it the one image-paste
    /// surface that stays live with nothing selected.
    @Test("The backdrop row needs only a picture and a writable board")
    func theBackgroundRowsClauses() throws {
        let harness = try makeClipboardHarness()
        defer { harness.tearDown() }
        harness.pasteboard.seed([(UTType.png.identifier, encodedImage(.png))])
        harness.clipboard.refresh()

        harness.store.clearSelection()
        #expect(harness.clipboard.canPasteBoardBackground(into: harness.store))
        #expect(!harness.clipboard.canPasteImage(into: harness.store), "no card selected")
    }

    @Test("The card-window branch refuses a card that is not on the board side")
    func theCardWindowBranchsClauses() throws {
        let harness = try makeClipboardHarness()
        defer { harness.tearDown() }
        harness.pasteboard.seed([(UTType.png.identifier, encodedImage(.png))])
        harness.clipboard.refresh()

        #expect(harness.clipboard.canPasteImage(intoCard: clipboardCard1, in: harness.store))
        #expect(!harness.clipboard.canPasteImage(intoCard: clipboardCard3, in: harness.store), "trashed")
        #expect(!harness.clipboard.canPasteImage(intoCard: clipboardLane1, in: harness.store), "a lane")
    }
}

// MARK: - Set as Hero

@MainActor
@Suite("The hero key ▸ set and remove")
struct SetAsHeroTests {

    private func heroKey(of card: String, lane: String, in fixture: WriterFixture) throws -> FieldValue<String> {
        try FrontmatterDocument.parse(fixture.indexText("\(lane)/\(card)")).hero
    }

    /// The reload between the two halves is not ceremony: the no-op guard reads the **snapshot**,
    /// which is one reload behind every write the app makes (the one-way flow), exactly as
    /// `applyStyle`'s `effective(_:against:)` does. The two surfaces stay consistent because they read
    /// the same snapshot — while it has not caught up, the row is still offering *Set* as Hero, so
    /// there is no Remove for a user to press.
    @Test("Set as Hero writes the bare filename; Remove Hero takes the key away")
    func setThenRemove() async throws {
        let fixture = try makeClipboardBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        #expect(store.setHero("photo.png", onCard: clipboardCard1))
        #expect(try heroKey(of: Ident.card1, lane: Ident.lane1, in: fixture).value == "photo.png")

        await settle(store)
        #expect(store.setHero(nil, onCard: clipboardCard1))
        #expect(try heroKey(of: Ident.card1, lane: Ident.lane1, in: fixture).isMissing)
        #expect(store.banners.oneShots.isEmpty)
    }

    /// One hero per card, and the user picked a different picture: Set replaces rather than refusing,
    /// so no Remove-then-Set dance.
    @Test("Set as Hero on a card that already has one replaces it")
    func setReplaces() async throws {
        let fixture = try makeClipboardBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        #expect(store.setHero("photo.png", onCard: clipboardCard1))
        await settle(store)
        #expect(store.setHero("notes.txt", onCard: clipboardCard1))
        #expect(try heroKey(of: Ident.card1, lane: Ident.lane1, in: fixture).value == "notes.txt")
    }

    @Test("Writing the hero a card already has is a no-op — no write, no step")
    func redundantSetsAreFree() async throws {
        let fixture = try makeClipboardBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let history = NativeHistoryProvider()
        store.history = history

        // Removing a hero that was never there changes nothing.
        #expect(!store.setHero(nil, onCard: clipboardCard1))
        #expect(!history.canUndo, "nothing happened, so nothing is on the stack")

        #expect(store.setHero("photo.png", onCard: clipboardCard1))
        await settle(store)
        #expect(!store.setHero("photo.png", onCard: clipboardCard1), "already says exactly this")
    }

    @Test("The other keys and the body ride through untouched")
    func everythingElseSurvives() throws {
        let fixture = try makeClipboardBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        #expect(store.setHero("photo.png", onCard: clipboardCard1))
        let document = try FrontmatterDocument.parse(fixture.indexText("\(Ident.lane1)/\(Ident.card1)"))
        #expect(document.title.value == "First")
        #expect(document.value(for: "project") != nil, "an unknown key is round-tripped")
        #expect(document.body.contains("First body"))
    }

    @Test("A trashed card and a lane are both refused")
    func containerGuards() throws {
        let fixture = try makeClipboardBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        #expect(!store.setHero("photo.png", onCard: clipboardCard3), "in the trash")
        #expect(!store.setHero("photo.png", onCard: clipboardLane1), "a lane has no hero")
        #expect(try FrontmatterDocument.parse(fixture.indexText(".trash/\(Ident.card3)")).hero.isMissing)
    }

    @Test("⌘Z puts the key back exactly as it was, under the restyle phrase")
    func undoRestoresTheKey() throws {
        let fixture = try makeClipboardBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let history = NativeHistoryProvider()
        store.history = history

        #expect(store.setHero("photo.png", onCard: clipboardCard1))
        #expect(try heroKey(of: Ident.card1, lane: Ident.lane1, in: fixture).value == "photo.png")
        #expect(history.undoActionName == "Restyle Card")

        history.undo()
        #expect(try heroKey(of: Ident.card1, lane: Ident.lane1, in: fixture).isMissing)

        history.redo()
        #expect(try heroKey(of: Ident.card1, lane: Ident.lane1, in: fixture).value == "photo.png")
    }

    /// A prior hero comes back as itself rather than as an absence — `applyStyle`'s inverse reading.
    @Test("Undoing a replacement restores the hero that was there")
    func undoRestoresAPriorHero() async throws {
        let fixture = try makeClipboardBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let history = NativeHistoryProvider()
        store.history = history

        #expect(store.setHero("photo.png", onCard: clipboardCard1))
        await settle(store)
        #expect(store.setHero("notes.txt", onCard: clipboardCard1))

        history.undo()
        #expect(try heroKey(of: Ident.card1, lane: Ident.lane1, in: fixture).value == "photo.png")
    }

    /// The wiring, driven through the real `configureAttachments` so the test breaks if the seam is
    /// ever crossed or the id captured from the wrong place.
    @Test("The attachment row's seam writes the window's own card")
    func theRowsSeam() throws {
        let harness = try makeClipboardHarness()
        defer { harness.tearDown() }

        let attachments = CardAttachments()
        CardWindowHost.configureAttachments(
            attachments, store: harness.store, cardID: clipboardCard1, undo: CardWindowUndo(),
            clipboard: harness.clipboard
        )
        attachments.setHeroFile?("photo.png")

        #expect(try heroKey(of: Ident.card1, lane: Ident.lane1, in: harness.fixture).value == "photo.png")
    }
}

// MARK: - The card window's paste-image affordance

/// **The header's control, wired through `configureAttachments`** — replaces the retired ⌘V
/// image-data branch (04-interactions.md ▸ Clipboard, re-ruled 2026-08-09). Driven through the real
/// wiring, `theRowsSeam`'s own reason: the test breaks if the seam is ever crossed or the id captured
/// from the wrong place.
@MainActor
@Suite("Paste ▸ the card window's paste-image affordance")
struct PasteImageAffordanceTests {

    private func attachments(_ card: String, in fixture: WriterFixture) throws -> [String] {
        let model = try BoardLoader.load(boardRoot: fixture.root).model
        for lane in model.lanes {
            if let match = lane.cards.first(where: { $0.id.rawValue == card }) { return match.attachments }
        }
        return []
    }

    @Test("The button's write lands the pasteboard's picture on the window's own card")
    func theButtonsWrite() throws {
        let harness = try makeClipboardHarness()
        defer { harness.tearDown() }
        harness.pasteboard.seed([(UTType.png.identifier, encodedImage(.png))])
        harness.clipboard.refresh()

        let attachments = CardAttachments()
        CardWindowHost.configureAttachments(
            attachments, store: harness.store, cardID: clipboardCard4, undo: CardWindowUndo(),
            clipboard: harness.clipboard
        )
        #expect(attachments.canPasteImage?() == true)

        attachments.pasteImage?()

        #expect(try self.attachments(Ident.card4, in: harness.fixture) == ["Pasted Image.png"])
    }

    @Test("The button's visibility clears once the pasteboard no longer offers a picture")
    func visibilityFollowsThePasteboard() throws {
        let harness = try makeClipboardHarness()
        defer { harness.tearDown() }

        let attachments = CardAttachments()
        CardWindowHost.configureAttachments(
            attachments, store: harness.store, cardID: clipboardCard1, undo: CardWindowUndo(),
            clipboard: harness.clipboard
        )
        #expect(attachments.canPasteImage?() == false, "nothing on the pasteboard yet")

        harness.pasteboard.seed([(UTType.png.identifier, encodedImage(.png))])
        harness.clipboard.refresh()
        #expect(attachments.canPasteImage?() == true)

        // A Finder-copied file outranks the picture riding beside it — the same precedence ⌘V's own
        // file branch reads (`PastedImage.flavor`'s clause order) — so the button goes quiet exactly
        // where the file branch lights up instead.
        harness.pasteboard.seedFileURLs(
            [URL(fileURLWithPath: "/tmp/shot.png")], also: [(UTType.png.identifier, encodedImage(.png))]
        )
        harness.clipboard.refresh()
        #expect(attachments.canPasteImage?() == false, "a file URL wins the precedence")
    }

    @Test("The button is absent under the read-only lock")
    func lockedBoardsOfferNothing() throws {
        let harness = try makeClipboardHarness()
        defer { harness.tearDown() }
        harness.pasteboard.seed([(UTType.png.identifier, encodedImage(.png))])
        harness.clipboard.refresh()
        harness.store.enterVanishedRootLock()

        let attachments = CardAttachments()
        CardWindowHost.configureAttachments(
            attachments, store: harness.store, cardID: clipboardCard1, undo: CardWindowUndo(),
            clipboard: harness.clipboard
        )
        #expect(attachments.canPasteImage?() == false)
    }
}

// MARK: - The card window's ⌘V routing, image data retired

/// **`CardWindowPasteRouting.action` keeps one clause** (04-interactions.md ▸ Clipboard, re-ruled
/// 2026-08-09: "instead of a special ⌘V handler at card window level … add a control"). Driven
/// directly rather than through `canPasteFiles`/`canPasteImage` on their own, because those two
/// predicates staying correct says nothing about whether the *composition* still offers the retired
/// branch — which is exactly the regression this suite exists to catch.
@MainActor
@Suite("Paste ▸ the card window's ⌘V, image data retired")
struct CardWindowPasteRoutingTests {

    private func attachmentNames(_ card: String, in fixture: WriterFixture) throws -> [String] {
        let model = try BoardLoader.load(boardRoot: fixture.root).model
        for lane in model.lanes {
            if let match = lane.cards.first(where: { $0.id.rawValue == card }) { return match.attachments }
        }
        return []
    }

    @Test("Raw image data alone offers the card window's ⌘V nothing")
    func imageDataAloneOffersNothing() throws {
        let harness = try makeClipboardHarness()
        defer { harness.tearDown() }
        harness.pasteboard.seed([(UTType.png.identifier, encodedImage(.png))])
        harness.clipboard.refresh()

        #expect(
            harness.clipboard.canPasteImage(intoCard: clipboardCard4, in: harness.store),
            "the affordance would still show"
        )
        let action = CardWindowPasteRouting.action(
            store: harness.store, cardID: clipboardCard4, clipboard: harness.clipboard
        )
        #expect(action == nil)
        #expect(try attachmentNames(Ident.card4, in: harness.fixture).isEmpty)
    }

    @Test("A Finder-copied file still routes through the card window's ⌘V")
    func fileURLsStillRoute() throws {
        let harness = try makeClipboardHarness()
        defer { harness.tearDown() }
        let source = try WriterFixture()
        defer { source.tearDown() }
        let shot = try source.file("shot.png", Data([0x89, 0x50]))
        harness.pasteboard.seedFileURLs([shot])
        harness.clipboard.refresh()

        let action = CardWindowPasteRouting.action(
            store: harness.store, cardID: clipboardCard4, clipboard: harness.clipboard
        )
        #expect(action != nil)
        action?()

        #expect(try attachmentNames(Ident.card4, in: harness.fixture) == ["shot.png"])
    }
}

// MARK: - The hero rows' menu rules

@Suite("The hero rows ▸ menu validation")
struct HeroMenuRulesTests {

    private let names = ["photo.png", "notes.txt", "shot.jpeg"]

    @Test("Set as Hero is offered on an image row that is not already the hero")
    func setIsOffered() {
        #expect(CardAttachments.canSetHero("photo.png", hero: nil, names: names, isEditable: true))
        #expect(CardAttachments.canSetHero("shot.jpeg", hero: "photo.png", names: names, isEditable: true))
    }

    @Test("It is absent on the current hero's row, which shows Remove Hero instead")
    func theCurrentHerosRow() {
        #expect(!CardAttachments.canSetHero("photo.png", hero: "photo.png", names: names, isEditable: true))
        #expect(CardAttachments.canRemoveHero("photo.png", hero: "photo.png", isEditable: true))
        #expect(!CardAttachments.canRemoveHero("shot.jpeg", hero: "photo.png", isEditable: true))
    }

    /// Offering the row on a `.zip` would let a user set a hero that can never draw.
    @Test("A non-image row is never offered Set as Hero")
    func nonImageRows() {
        #expect(!CardAttachments.canSetHero("notes.txt", hero: nil, names: names, isEditable: true))
    }

    /// A hero somebody hand-wrote to a non-image file is exactly the state Remove Hero exists to get
    /// out of, so that row does *not* repeat the image test.
    @Test("Remove Hero is offered even where Set as Hero would not be")
    func removeDoesNotRepeatTheImageTest() {
        #expect(CardAttachments.canRemoveHero("notes.txt", hero: "notes.txt", isEditable: true))
    }

    @Test("The lock closes both rows, and a row not in the listing offers nothing")
    func theLockAndTheListing() {
        #expect(!CardAttachments.canSetHero("photo.png", hero: nil, names: names, isEditable: false))
        #expect(!CardAttachments.canRemoveHero("photo.png", hero: "photo.png", isEditable: false))
        #expect(!CardAttachments.canSetHero("gone.png", hero: nil, names: names, isEditable: true))
    }
}

// MARK: - The menu-bar twins

/// The two File rows the context entry's twin contract requires (11-command-nexus.md: "no function's
/// only home"), validated as values rather than by driving a menu — `AddAttachmentCommand`'s own
/// shape, for its reason.
@MainActor
@Suite("File ▸ Set as Hero / Remove Hero")
struct HeroCommandTests {

    private func section(selected: String?, hero: String?) -> CardAttachments {
        let attachments = CardAttachments()
        attachments.names = ["photo.png", "notes.txt", "shot.jpeg"]
        attachments.hero = hero
        attachments.selected = selected
        attachments.isEditable = true
        return attachments
    }

    @Test("They read the section's selected row, and are dead with nothing selected")
    func theyFollowTheSelection() {
        #expect(SetAsHeroCommand.isEnabled(section(selected: "photo.png", hero: nil)))
        #expect(!SetAsHeroCommand.isEnabled(section(selected: nil, hero: nil)))
        #expect(!RemoveHeroCommand.isEnabled(section(selected: nil, hero: "photo.png")))
        #expect(!SetAsHeroCommand.isEnabled(nil), "no card window in front, no row")
        #expect(!RemoveHeroCommand.isEnabled(nil))
    }

    /// Exactly one of the pair is ever live on a given row, which is what makes them read as one
    /// gesture with two directions rather than as two independent commands.
    @Test("Only one of the pair is live on any row")
    func onlyOneIsLive() {
        let onTheHero = section(selected: "photo.png", hero: "photo.png")
        #expect(!SetAsHeroCommand.isEnabled(onTheHero))
        #expect(RemoveHeroCommand.isEnabled(onTheHero))

        let onAnother = section(selected: "shot.jpeg", hero: "photo.png")
        #expect(SetAsHeroCommand.isEnabled(onAnother))
        #expect(!RemoveHeroCommand.isEnabled(onAnother))

        let onAFile = section(selected: "notes.txt", hero: "photo.png")
        #expect(!SetAsHeroCommand.isEnabled(onAFile))
        #expect(!RemoveHeroCommand.isEnabled(onAFile))
    }

    @Test("The lock closes both rows")
    func theLock() {
        let locked = section(selected: "photo.png", hero: "photo.png")
        locked.isEditable = false
        #expect(!SetAsHeroCommand.isEnabled(locked))
        #expect(!RemoveHeroCommand.isEnabled(locked))
    }
}

// MARK: - Paste as Board Background

@MainActor
@Suite("Paste ▸ as board background")
struct PasteBoardBackgroundTests {

    private func background(_ fixture: WriterFixture) throws -> FrontmatterDocument {
        try FrontmatterDocument.parse(fixture.indexText(""))
    }

    @Test("The picture lands in .backgrounds/ and `background.image` names it")
    func thePictureLands() throws {
        let harness = try makeClipboardHarness()
        defer { harness.tearDown() }
        let png = encodedImage(.png)
        harness.pasteboard.seed([(UTType.png.identifier, png)])

        #expect(harness.clipboard.pasteBoardBackground(into: harness.store))

        #expect(try harness.fixture.data(".backgrounds/Pasted Background.png") == png)
        #expect(try background(harness.fixture).backgroundImage.value == ".backgrounds/Pasted Background.png")
        #expect(harness.store.banners.oneShots.isEmpty)
    }

    /// A picture off the pasteboard carries no ground colour, so the gesture writes none — and a
    /// colour the board already had survives, which is `setBackgroundImage`'s per-subkey contract.
    @Test("The colour subkey is left exactly as it was")
    func theColourSurvives() async throws {
        let harness = try makeClipboardHarness()
        defer { harness.tearDown() }
        #expect(harness.store.applySolidBackground(colorHex: "#112233"))
        await settle(harness.store)
        harness.pasteboard.seed([(UTType.png.identifier, encodedImage(.png))])

        #expect(harness.clipboard.pasteBoardBackground(into: harness.store))

        let document = try background(harness.fixture)
        #expect(document.background.value == "#112233")
        #expect(document.backgroundImage.value == ".backgrounds/Pasted Background.png")
    }

    /// The generator's overwrite-in-place rule, inherited: re-pasting must not leave a folder full of
    /// abandoned pictures.
    @Test("Re-pasting overwrites the board's own file rather than climbing the ladder")
    func rePastingOverwrites() async throws {
        let harness = try makeClipboardHarness()
        defer { harness.tearDown() }
        harness.pasteboard.seed([(UTType.png.identifier, encodedImage(.png, side: 4))])
        #expect(harness.clipboard.pasteBoardBackground(into: harness.store))
        await settle(harness.store)

        let second = encodedImage(.png, side: 8)
        harness.pasteboard.seed([(UTType.png.identifier, second)])
        #expect(harness.clipboard.pasteBoardBackground(into: harness.store))

        #expect(try harness.fixture.data(".backgrounds/Pasted Background.png") == second)
        #expect(!harness.fixture.exists(".backgrounds/Pasted Background 2.png"))
    }

    /// A hand-placed file of that name **inside `.backgrounds/`** is the user's, and is never written
    /// through — the ladder's rule, the same one the generator follows. A file of that name at board
    /// root would not collide at all any more: it simply is not where this write ever looks.
    @Test("A file already holding the name is stepped around")
    func anExistingNameIsRespected() throws {
        let harness = try makeClipboardHarness()
        defer { harness.tearDown() }
        let mine = Data("hand placed".utf8)
        try harness.fixture.file(".backgrounds/Pasted Background.png", mine)
        harness.pasteboard.seed([(UTType.png.identifier, encodedImage(.png))])

        #expect(harness.clipboard.pasteBoardBackground(into: harness.store))

        #expect(try harness.fixture.data(".backgrounds/Pasted Background.png") == mine)
        #expect(try background(harness.fixture).backgroundImage.value == ".backgrounds/Pasted Background 2.png")
    }

    /// The two producers must not read each other's echo: a paste landing inside the reroll's window
    /// must not overwrite `facets.png`.
    @Test("A paste right after a generated background writes its own file")
    func theTwoProducersStayApart() throws {
        let harness = try makeClipboardHarness()
        defer { harness.tearDown() }
        let generated = encodedImage(.png, side: 4)
        #expect(harness.store.applyGeneratedBackground(png: generated, colorHex: "#445566"))
        // Deliberately *no* reload — this is the echo window the generator's memo exists for.
        harness.pasteboard.seed([(UTType.png.identifier, encodedImage(.png, side: 8))])

        #expect(harness.clipboard.pasteBoardBackground(into: harness.store))

        #expect(try harness.fixture.data(".backgrounds/\(FacetsGenerator.fileName)") == generated, "untouched")
        #expect(try background(harness.fixture).backgroundImage.value == ".backgrounds/Pasted Background.png")
    }

    /// `theTwoProducersStayApart`'s settled counterpart — the orphan tidy's replace-in-place trim
    /// (ruled 2026-08-09): once the generate has *settled* (a reload landed and `snapshot` caught up
    /// with it), a paste that repoints away from it trims the generated file, on the same terms
    /// `GeneratedBackgroundTests.BackgroundReplaceInPlaceTrimTests` pins for the reverse direction.
    @Test("A paste after a settled generated background trims it")
    func aSettledPasteTrimsTheGeneratedFile() async throws {
        let harness = try makeClipboardHarness()
        defer { harness.tearDown() }
        let generated = encodedImage(.png, side: 4)
        #expect(harness.store.applyGeneratedBackground(png: generated, colorHex: "#445566"))
        await settle(harness.store)
        harness.pasteboard.seed([(UTType.png.identifier, encodedImage(.png, side: 8))])

        #expect(harness.clipboard.pasteBoardBackground(into: harness.store))

        #expect(!harness.fixture.exists(".backgrounds/\(FacetsGenerator.fileName)"), "trimmed by the settled paste")
        #expect(try background(harness.fixture).backgroundImage.value == ".backgrounds/Pasted Background.png")
        #expect(harness.store.banners.losses.isEmpty, "the in-flow trim is silent")
    }

    @Test("⌘Z puts the image subkey back and leaves the colour alone")
    func undoRestoresTheSubkey() throws {
        let fixture = try makeClipboardBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let history = NativeHistoryProvider()
        store.history = history

        #expect(store.applyPastedBackground(data: encodedImage(.png), fileExtension: "png"))
        #expect(try FrontmatterDocument.parse(fixture.indexText("")).backgroundImage.value
            == ".backgrounds/Pasted Background.png")
        #expect(history.undoActionName == "Restyle Board")

        history.undo()
        #expect(try FrontmatterDocument.parse(fixture.indexText("")).backgroundImage.isMissing)
        // The file survives the undo — "the undo restores the field, not the bytes".
        #expect(fixture.exists(".backgrounds/Pasted Background.png"))

        history.redo()
        #expect(try FrontmatterDocument.parse(fixture.indexText("")).backgroundImage.value
            == ".backgrounds/Pasted Background.png")
    }
}

// MARK: - The body editor's paste yield

/// The responder behind the editor in these tests — stands where the card window's
/// `cardWindowPaste` bridge stands in the app, and only counts.
@MainActor
private final class PasteCatcher: NSView {
    var pastes = 0
    @objc func paste(_ sender: Any?) { pastes += 1 }
}

/// **`CardBodyTextView` yields a paste it cannot read** (05-card-window.md ▸ Attachments, ruled
/// 2026-08-09) — a screenshot pasteboard reaches *whatever responds behind the editor* even while
/// the editor holds the keyboard, and a text paste never leaves the editor. The mechanism is
/// capability-based (`readablePasteboardTypes`), not a picture-specific rule, which is exactly what
/// lets `CardWindowPasteRouting.action`'s image branch retire without touching this file at all
/// (re-ruled 2026-08-09 — 04-interactions.md ▸ Clipboard): in the real app today nothing answers
/// `paste:` behind the editor for an image-only pasteboard any more, so a screenshot ⌘V with the
/// body editor focused is a no-op, served instead by the attachments header's control
/// (`CardPasteImageAffordance`). `PasteCatcher` here stands for "something behind the editor still
/// takes it" in the general case, which is the claim these tests actually pin.
///
/// The pasteboard is a private named one through the view's `yieldPasteboard` seam, so the suite
/// never reads the machine's — except through `super.paste`, which is AppKit's own and is exactly
/// why the text-path test asserts the catcher stayed silent rather than what landed in the view.
@MainActor
@Suite("Paste yield ▸ the body editor")
struct PasteYieldTests {

    private func makeEditor(behind catcher: PasteCatcher? = nil) -> (CardBodyTextView, NSPasteboard) {
        let pasteboard = NSPasteboard(name: NSPasteboard.Name("test-yield-\(UUID().uuidString)"))
        pasteboard.clearContents()
        let editor = CardBodyTextView(frame: .zero)
        editor.isRichText = false
        editor.isEditable = true
        editor.yieldPasteboard = pasteboard
        catcher?.addSubview(editor)
        return (editor, pasteboard)
    }

    private var pasteItem: NSMenuItem {
        NSMenuItem(title: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "")
    }

    @Test("An image-only pasteboard validates through the editor and forwards to the responder behind")
    func imageOnlyYields() {
        let catcher = PasteCatcher(frame: .zero)
        let (editor, pasteboard) = makeEditor(behind: catcher)
        defer { pasteboard.releaseGlobally() }
        pasteboard.setData(encodedImage(.png), forType: .png)

        #expect(editor.validateUserInterfaceItem(pasteItem))
        editor.paste(nil)
        #expect(catcher.pastes == 1)
    }

    /// **The file-URL branch's own yield** — a Finder copy reaches the window's attachment branch
    /// exactly as a screenshot does, `readablePasteboardTypes`'s explicit exclusion of `.fileURL`
    /// (`CardBodyTextView`'s override) proven rather than assumed: `importsGraphics` being `false` is
    /// an AppKit default this pins down as a contract.
    @Test("A file-URL-only pasteboard validates through the editor and forwards to the responder behind")
    func fileURLOnlyYields() throws {
        let catcher = PasteCatcher(frame: .zero)
        let (editor, pasteboard) = makeEditor(behind: catcher)
        defer { pasteboard.releaseGlobally() }
        let fileURL = FileManager.default.temporaryDirectory
            .appendingPathComponent("paste-yield-\(UUID().uuidString).png")
        try Data([0x01]).write(to: fileURL)
        defer { try? FileManager.default.removeItem(at: fileURL) }
        pasteboard.writeObjects([fileURL as NSURL])

        #expect(editor.validateUserInterfaceItem(pasteItem))
        editor.paste(nil)
        #expect(catcher.pastes == 1)
    }

    /// No expectation on `validateUserInterfaceItem` here: a readable pasteboard routes validation
    /// to `super`, and `NSTextView`'s own answer reads the *machine's* general pasteboard — asserting
    /// it would tie the test to whatever the host's clipboard happens to hold. The claim under test
    /// is the routing: a text flavor means the paste is the editor's, so nothing is forwarded.
    @Test("A text flavor keeps the paste in the editor — riding image or not")
    func textStaysTheEditors() {
        let catcher = PasteCatcher(frame: .zero)
        let (editor, pasteboard) = makeEditor(behind: catcher)
        defer { pasteboard.releaseGlobally() }
        pasteboard.setString("plain words", forType: .string)
        pasteboard.setData(encodedImage(.png), forType: .png)

        editor.paste(nil)
        #expect(catcher.pastes == 0)
    }

    @Test("Preview mode takes no paste at all, so the window's branch owns it outright")
    func previewYieldsEverything() {
        let catcher = PasteCatcher(frame: .zero)
        let (editor, pasteboard) = makeEditor(behind: catcher)
        defer { pasteboard.releaseGlobally() }
        editor.isEditable = false
        pasteboard.setData(encodedImage(.png), forType: .png)

        #expect(editor.validateUserInterfaceItem(pasteItem))
        editor.paste(nil)
        #expect(catcher.pastes == 1)
    }

    @Test("Nothing behind to take it means a disabled row, not a swallowed gesture")
    func noTargetDisables() {
        let (editor, pasteboard) = makeEditor()
        defer { pasteboard.releaseGlobally() }
        pasteboard.setData(encodedImage(.png), forType: .png)

        #expect(!editor.validateUserInterfaceItem(pasteItem))
    }
}
