import Foundation
import Testing
@testable import Kanban

/// The card window's raw-source outlet (05-card-window.md ▸ Raw source outlet) — the escape hatch
/// that "keeps *everything* — unknown keys, exotic formatting — reachable in-app".
///
/// Its promise is a byte one, so this suite reads bytes: what Apply writes must be the buffer's own
/// bytes and nothing else, which means **no `modified` stamp and no cleared `modified-by`** — the one
/// write in the app of which both are true (01-storage-format.md § Frontmatter settles each
/// explicitly). The negative half matters as much: a proposal the loader would refuse must leave the
/// file untouched, and "the loader" has to mean the same code the loader runs, not a second parser
/// that agrees with it today.
///
/// `WriterFixture`, `Ident`, `Item` and `writeFailure` come from `WriterTestSupport.swift`.

// MARK: - Fixture

/// The card the outlet opens, carrying everything a verbatim write must be able to preserve *and*
/// everything a stamping write would have destroyed: an unknown key with an inline comment, a second
/// unknown key in a shape the app never writes, an old `modified`, and a foreign `modified-by`.
private let editableCard = """
---
schema: 1
title: Notes
order: 1024
project: lanework       # agent overlay
labels: [a, b, c]
created: 2026-01-01T09:00:00Z
modified: 2026-02-02T09:00:00Z
modified-by: claude
---
# Notes

Some *prose*.

"""

/// A proposal in the shape only this outlet can produce: a hand-added key, a comment above it,
/// blank lines inside the frontmatter, an old `modified` left exactly as the user found it, a
/// `modified-by` they typed themselves — and **no final newline**, the byte a re-serializing writer
/// would have added back.
private let handEditedSource = """
---
schema: 1
title: Notes

# the user's own comment
sphere: work
project: lanework       # agent overlay
labels: [a, b, c]
order: 1024
created: 2026-01-01T09:00:00Z
modified: 2026-02-02T09:00:00Z
modified-by: rzen
---
# Notes

Rewritten by hand.
"""

private let cardPath = "\(Ident.lane1)/\(Ident.card1)"

@MainActor
private func makeBoard() throws -> WriterFixture {
    let fixture = try WriterFixture()
    try fixture.item("", Item.board)
    try fixture.item(Ident.lane1, Item.rich(order: "1024", title: "Todo"))
    try fixture.item(cardPath, editableCard)
    return fixture
}

private func modificationDate(_ fixture: WriterFixture, _ relativePath: String) throws -> Date? {
    let url = fixture.url(relativePath).appendingPathComponent("index.md")
    return try FileManager.default.attributesOfItem(atPath: url.path)[.modificationDate] as? Date
}

/// Filesystem timestamps are coarse; a write inside the same tick would be invisible to an `mtime`
/// assertion either way it goes.
private func settleClock() {
    Thread.sleep(forTimeInterval: 0.05)
}

// MARK: - Validation, through the loader's own parse

@MainActor
@Suite("Raw source ▸ validation")
struct RawSourceValidationTests {

    @Test("A card file with unknown keys and exotic formatting is valid — that is the whole point")
    func theOutletValidatesWhatItExistsToWrite() throws {
        let document = try BoardLoader.validateCardIndex(Data(handEditedSource.utf8), path: "index.md")

        #expect(document.title.value == "Notes")
        // Unknown keys are not merely tolerated, they are the outlet's reason for being: "the write
        // path for frontmatter the app doesn't own" (05 ▸ Details).
        #expect(document.value(for: "sphere") == .string("work"))
    }

    /// **A card's `schema` and `order` are both optional** (01-storage-format.md § Frontmatter and
    /// § Ordering, re-ruled 2026-07-31) — the outlet's gate is the loader's rule, so it moved with
    /// it: a card applied without either lands at its lane's bottom, read as schema 1, and gains a
    /// real rank on its next touch.
    @Test("The optional fields are optional, with the loader's own rule")
    func schemaAndOrderAreOptional() throws {
        for text in ["---\ntitle: x\norder: 1\n---\nbody\n", "---\nschema: 1\ntitle: x\n---\nbody\n", "---\ntitle: x\n---\nbody\n"] {
            #expect(throws: Never.self) {
                try BoardLoader.validateCardIndex(Data(text.utf8), path: "index.md")
            }
        }

        // What still refuses: a value that is there and unreadable, and a card from a newer app —
        // which this one has no honest way to rewrite.
        let malformed = validationFailure("---\nschema: one\norder: 1\n---\nbody\n")
        #expect(malformed?.reason == .malformedSchema(raw: "one"))

        let future = validationFailure("---\nschema: 99\norder: 1\n---\nbody\n")
        #expect(future?.reason == .schemaNewerThanApp(found: 99))
    }

    @Test("Malformed YAML refuses with the loader's error, line number and all")
    func malformedYAMLRefuses() {
        let error = validationFailure("---\nschema: 1\norder: 1\n  bad: [unclosed\n---\nbody\n")

        guard case let .unparseableYAML(_, line) = error?.reason else {
            Issue.record("expected unparseable YAML, got \(String(describing: error?.reason))")
            return
        }
        // The taxonomy's display text is what the alert shows — "detailed alert" means this detail.
        #expect(error?.reason.description.contains("unparseable YAML") == true)
        #expect(line != nil, "the alert names the line the user has to go and look at")
    }

    @Test("A file with no frontmatter at all refuses")
    func missingDelimitersRefuse() {
        let error = validationFailure("just a body, no frontmatter\n")
        #expect(error?.reason.description.contains("---") == true)
    }

    @Test("A BOM is refused — the same rejection the loader makes, not a second rule")
    func aBOMIsRefused() {
        var bommed = Data([0xEF, 0xBB, 0xBF])
        bommed.append(Data("---\nschema: 1\norder: 1\n---\nbody\n".utf8))

        let error = validationFailureData(bommed)
        // "A BOM'd file fails the frontmatter delimiter and is rejected the same way, deliberately"
        // (01-storage-format.md § Fractal layout ▸ Rules) — so the refusal is the delimiter's, which
        // is exactly what proves the outlet is not re-implementing the encoding rules.
        #expect(error?.reason.description.contains("---") == true)
    }

    @Test("Bytes that are not UTF-8 are refused before anything tries to parse them")
    func invalidUTF8IsRefused() {
        var invalid = Data("---\nschema: 1\norder: 1\ntitle: ".utf8)
        invalid.append(contentsOf: [0xFF, 0xFE, 0x80])
        invalid.append(Data("\n---\nbody\n".utf8))

        let error = validationFailureData(invalid)
        #expect(error?.reason == .unparseableYAML(message: "file is not UTF-8", line: nil))
    }

    @Test("A flow-mapping frontmatter is valid here, though no other write in the app may touch it")
    func theUneditableShapeIsWritableAsSource() throws {
        // `BoardWriter.writeBody` refuses this shape — a surgical span edit of it cannot be
        // expressed. Raw source replaces the whole file, so it is precisely the surface that can get
        // a user *out* of such a file. Nothing here checks `uneditableShape`, deliberately.
        let document = try BoardLoader.validateCardIndex(
            Data("---\n{schema: 1, order: 1024, title: Odd}\n---\nodd body\n".utf8),
            path: "index.md"
        )
        #expect(document.uneditableShape != nil, "still uneditable in place — and still writable as source")
    }

    private func validationFailure(_ text: String) -> BoardLoadError? {
        validationFailureData(Data(text.utf8))
    }

    private func validationFailureData(_ data: Data) -> BoardLoadError? {
        do {
            _ = try BoardLoader.validateCardIndex(data, path: "index.md")
            Issue.record("expected the validation to refuse, but it passed")
            return nil
        } catch {
            return error
        }
    }
}

// MARK: - The Writer

@MainActor
@Suite("BoardWriter ▸ raw source")
struct RawSourceWriterTests {

    @Test("Reading gives the file's literal text — comments, CRLF, missing final newline and all")
    func theReadIsByteHonest() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("", Item.board)
        try fixture.item(Ident.lane1, Item.rich(order: "1024", title: "Todo"))
        // No final newline, mixed line endings, a comment: everything a round-trip through a parser
        // might tidy away.
        let odd = "---\r\nschema: 1\r\norder: 1024\n# comment\ntitle: Odd\r\n---\r\nbody with no final newline"
        try fixture.item(cardPath, odd)

        #expect(try BoardWriter.readRawSource(ofCard: fixture.url(cardPath)) == odd)
    }

    @Test("A file that is not UTF-8 refuses to open as source rather than opening a lie")
    func nonUTF8RefusesToOpen() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        var bytes = Data("---\nschema: 1\norder: 1024\ntitle: ".utf8)
        bytes.append(contentsOf: [0xFF, 0xFE])
        bytes.append(Data("\n---\nbody\n".utf8))
        try fixture.item(cardPath, bytes: bytes)

        var thrown: BoardWriteError?
        do {
            _ = try BoardWriter.readRawSource(ofCard: fixture.url(cardPath))
            Issue.record("expected the read to refuse")
        } catch {
            thrown = error
        }
        #expect(thrown?.reason == .unreadable(message: "file is not UTF-8"))
    }

    @Test("Only a card's file is reachable — not a lane's, not the board's")
    func onlyCardsAreReachable() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }

        for folder in [fixture.root, fixture.url(Ident.lane1)] {
            let error = writeFailure { try BoardWriter.writeRawSource(inCard: folder, text: editableCard) }
            if case .unreadable = error?.reason {} else {
                Issue.record("expected an unreadable refusal, got \(String(describing: error?.reason))")
            }
        }
    }

    @Test("Apply writes the buffer's bytes exactly — no stamp, no cleared modified-by, no added newline")
    func applyIsByteForByte() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }

        let wrote = try BoardWriter.writeRawSource(inCard: fixture.url(cardPath), text: handEditedSource)

        #expect(wrote)
        // The whole contract in one assertion: what is on disk *is* the proposal.
        #expect(try fixture.indexData(cardPath) == Data(handEditedSource.utf8))
        // And the three things a composed app write would have done to it, spelled out because each
        // is settled prose rather than an implementation detail:
        let after = try fixture.indexText(cardPath)
        // 1. `modified` untouched — "Two designed app writes therefore don't bump it, deliberately:
        //    **raw-source Apply** writes the validated buffer byte-for-byte (the verbatim contract
        //    outranks stamping)" (01-storage-format.md § Frontmatter).
        #expect(after.contains("modified: 2026-02-02T09:00:00Z"))
        // 2. `modified-by` kept — "One carve-out: **raw-source Apply** … writes byte-for-byte and does
        //    *not* clear a stamp the user typed or kept" (01), and 05 from the other side.
        #expect(after.contains("modified-by: rzen"))
        // 3. No final newline invented.
        #expect(!after.hasSuffix("\n"))
    }

    @Test("Byte-for-byte still means the file was rewritten — mtime moves, content is the proposal")
    func aRealApplyTouchesTheFile() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let before = try modificationDate(fixture, cardPath)
        settleClock()

        try BoardWriter.writeRawSource(inCard: fixture.url(cardPath), text: handEditedSource)

        // The stamping choice is about *content*: `modified` describes the item, `mtime` describes
        // the file, and the file genuinely changed. Pinning both directions is what keeps a future
        // "helpful" stamp from passing the byte assertion by luck.
        #expect(try modificationDate(fixture, cardPath) != before)
        #expect(try fixture.indexData(cardPath) == Data(handEditedSource.utf8))
    }

    @Test("Applying the text the file already has writes nothing at all — bytes and mtime")
    func anUnchangedBufferIsNeverReSerialized() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let before = try fixture.indexData(cardPath)
        let mtime = try modificationDate(fixture, cardPath)
        settleClock()

        let wrote = try BoardWriter.writeRawSource(inCard: fixture.url(cardPath), text: editableCard)

        #expect(!wrote, "Apply on a buffer that was only read must not churn the file")
        #expect(try fixture.indexData(cardPath) == before)
        #expect(try modificationDate(fixture, cardPath) == mtime)
    }

    @Test("A proposal that would not load is refused, and the file keeps every byte it had")
    func aRefusedProposalWritesNothing() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let before = try fixture.indexData(cardPath)
        let mtime = try modificationDate(fixture, cardPath)
        settleClock()

        let error = writeFailure {
            try BoardWriter.writeRawSource(
                inCard: fixture.url(cardPath),
                text: "---\nschema: 1\n  order: [unclosed\n---\nbody\n"
            )
        }

        guard case let .invalidSource(loadError) = error?.reason else {
            Issue.record("expected an invalid-source refusal, got \(String(describing: error?.reason))")
            return
        }
        if case .unparseableYAML = loadError.reason {} else {
            Issue.record("expected the loader's own parse error, got \(loadError.reason)")
        }
        #expect(try fixture.indexData(cardPath) == before)
        #expect(try modificationDate(fixture, cardPath) == mtime)
    }

    @Test("A BOM'd proposal is refused by the same rule, and writes nothing")
    func aBOMdProposalWritesNothing() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let before = try fixture.indexData(cardPath)

        let error = writeFailure {
            try BoardWriter.writeRawSource(inCard: fixture.url(cardPath), text: "\u{FEFF}" + editableCard)
        }

        if case .invalidSource = error?.reason {} else {
            Issue.record("expected an invalid-source refusal, got \(String(describing: error?.reason))")
        }
        #expect(try fixture.indexData(cardPath) == before)
    }

    @Test("A failure names the card by the title the file still carries")
    func failuresNameTheCard() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let folder = fixture.url(cardPath)
        // Unwritable folder: the read, the title enrichment and the validation all succeed, and then
        // the atomic replace cannot land its temp file.
        try FileManager.default.setAttributes([.posixPermissions: 0o500], ofItemAtPath: folder.path)

        let error = writeFailure {
            try BoardWriter.writeRawSource(inCard: folder, text: handEditedSource)
        }

        // The title on the window — read off disk, never off the buffer, which may propose a name
        // that never landed (`WriteOperation.rawSource`).
        #expect(error?.operation == .rawSource(title: "Notes"))
        #expect(
            BannerCenter.headline(for: try #require(error))
                .hasPrefix("Couldn't apply source changes to 'Notes'")
        )
    }

    @Test("A flow-mapping card — unwritable by every other path — is rewritable as source")
    func theEscapeHatchEscapes() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let path = "\(Ident.lane1)/\(Ident.card3)"
        try fixture.item(path, "---\n{schema: 1, order: 3072, title: Odd}\n---\nodd body\n")
        // `writeBody` refuses this file outright; the outlet is how a user gets out of it.
        #expect(writeFailure { try BoardWriter.writeBody(inItemFolder: fixture.url(path), body: "new") } != nil)

        let repaired = "---\nschema: 1\norder: 3072\ntitle: Odd\n---\nodd body\n"
        try BoardWriter.writeRawSource(inCard: fixture.url(path), text: repaired)

        #expect(try fixture.indexData(path) == Data(repaired.utf8))
    }
}

// MARK: - Through the store

@MainActor
@Suite("BoardStore ▸ raw source")
struct RawSourceStoreTests {

    @Test("Reading gives the file, and Apply puts the buffer on disk verbatim")
    func theStoreReadsAndApplies() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let card = ItemID(rawValue: Ident.card1)

        #expect(store.readCardSource(inCard: card) == .read(editableCard))
        #expect(store.applyCardSource(inCard: card, text: handEditedSource) == .applied)
        // Read back off disk, never through the snapshot: the one-way flow means the snapshot only
        // catches up when the watcher's reload lands (02-architecture.md).
        #expect(try fixture.indexData(cardPath) == Data(handEditedSource.utf8))
    }

    @Test("Applying the file's own text reports unchanged and writes nothing")
    func anUnchangedApplyWritesNothing() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let before = try fixture.indexData(cardPath)
        let mtime = try modificationDate(fixture, cardPath)
        settleClock()

        #expect(store.applyCardSource(inCard: ItemID(rawValue: Ident.card1), text: editableCard) == .unchanged)
        #expect(try fixture.indexData(cardPath) == before)
        #expect(try modificationDate(fixture, cardPath) == mtime)
    }

    @Test("An invalid buffer is refused before any bracket opens — no write, no banner")
    func anInvalidBufferPostsNothing() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let before = try fixture.indexData(cardPath)

        let outcome = store.applyCardSource(
            inCard: ItemID(rawValue: Ident.card1),
            text: "---\nschema: unreadable\norder: 1\n---\nbody\n"
        )

        #expect(outcome == .invalid(BoardLoadError(path: "index.md", reason: .malformedSchema(raw: "unreadable"))))
        #expect(try fixture.indexData(cardPath) == before)
        // The alert is the surfacing for this one — a banner as well would say the same thing twice,
        // and a write that never started is not a failed write.
        #expect(store.banners.oneShots.isEmpty)
    }

    @Test("A tombstoned card takes no Apply — a stale buffer never undeletes a card")
    func aTombstonedCardIsRefused() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        try fixture.move(cardPath, toTrash: Ident.card1)
        let store = try BoardStore(rootURL: fixture.root)
        let before = try fixture.indexData(".trash/\(Ident.card1)")
        let card = ItemID(rawValue: Ident.card1)

        // 05 ▸ Deletion & lifecycle: "An open raw-source buffer discards instead: its Apply would
        // write a whole stale `index.md` over the trashed card — a delete is never fought by a stale
        // buffer." The Edit buffer's flush is the opposite rule, deliberately, which is why the two
        // walks differ (`boardItem` vs `cardBodyTarget`).
        #expect(store.readCardSource(inCard: card) == .vanished)
        #expect(store.applyCardSource(inCard: card, text: handEditedSource) == .vanished)
        #expect(try fixture.indexData(".trash/\(Ident.card1)") == before)
    }

    @Test("A card that is not in the board at all is vanished too")
    func anAbsentCardIsVanished() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        #expect(store.readCardSource(inCard: ItemID(rawValue: Ident.card4)) == .vanished)
        #expect(store.applyCardSource(inCard: ItemID(rawValue: Ident.card4), text: handEditedSource) == .vanished)
    }

    @Test("A read-only board suspends the Apply rather than failing it")
    func theLockSuspends() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        let before = try fixture.indexData(cardPath)
        let card = ItemID(rawValue: Ident.card1)
        store.enterVanishedRootLock()

        // Apply is a mutation, however literal, so the lock refuses it like every other write — and
        // the buffer's owner reads this as "hold the text", the standing lock row being the message.
        #expect(store.applyCardSource(inCard: card, text: handEditedSource) == .suspended(.vanishedRoot))
        #expect(try fixture.indexData(cardPath) == before)
        #expect(store.banners.oneShots.isEmpty, "the lock's row is the message; a refused Apply posts nothing")
        // Reading is not a write: a locked board still opens its source, which is how the text gets
        // copied out (02-architecture.md § the lock's scope).
        #expect(store.readCardSource(inCard: card) == .read(editableCard))
    }

    @Test("A failed write reports the error, and the banner has it")
    func aFailedApplyReports() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)
        try FileManager.default.setAttributes(
            [.posixPermissions: 0o500],
            ofItemAtPath: fixture.url(cardPath).path
        )

        let outcome = store.applyCardSource(inCard: ItemID(rawValue: Ident.card1), text: handEditedSource)

        guard case let .failed(error) = outcome else {
            Issue.record("expected a failure, got \(outcome)")
            return
        }
        #expect(error.operation == .rawSource(title: "Notes"))
        #expect(store.banners.oneShots.contains {
            BannerCenter.headline(for: $0.error).hasPrefix("Couldn't apply source changes to 'Notes'")
        })
    }
}

// MARK: - The session

/// The outlet's state machine, driven through **the window's own wiring**
/// (`CardWindowHost.configureRawSource`) rather than a re-typed copy of it: the ordering it encodes —
/// flush, *then* read fresh — is invisible in a running window until it is wrong.
@MainActor
@Suite("Card raw source ▸ session")
struct RawSourceSessionTests {

    /// Everything a card window holds, wired exactly as the host wires it.
    private struct Rig {
        let fixture: WriterFixture
        let store: BoardStore
        let presentation: CardBodyPresentation
        let body: CardBodyEditSession
        let raw: CardRawSourceSession
    }

    private func makeRig() throws -> Rig {
        let fixture = try makeBoard()
        let store = try BoardStore(rootURL: fixture.root)
        let cardID = ItemID(rawValue: Ident.card1)

        let presentation = CardBodyPresentation()
        let body = CardBodyEditSession()
        body.save = { [weak store] text in
            guard let store else { return .vanished }
            return store.writeCardBody(inCard: cardID, body: text)
        }
        body.adopt(diskBody: try FrontmatterDocument.parse(fixture.indexText(cardPath)).body)
        // The title field's own session — not this suite's subject, but `configureRawSource` now
        // flushes it alongside the body, so the wiring under test needs one to flush.
        let title = CardTitleEditSession()
        title.save = { [weak store] title in
            guard let store else { return .vanished }
            return store.commitCardTitle(inCard: cardID, title: title)
        }
        presentation.flushEdits = {
            body.endEditSession()
            title.endEditSession()
        }

        let raw = CardRawSourceSession()
        CardWindowHost.configureRawSource(raw, body: body, title: title, presentation: presentation, store: store, cardID: cardID)

        return Rig(fixture: fixture, store: store, presentation: presentation, body: body, raw: raw)
    }

    @Test("Entering flushes the Edit buffer first, then reads what the flush put on disk")
    func enteringFlushesThenReadsFresh() throws {
        let rig = try makeRig()
        defer { rig.fixture.tearDown() }
        rig.presentation.setMode(.edit)
        // Unsaved keystrokes, well inside the debounce: nothing has reached disk yet.
        rig.body.edited("# Notes\n\nTyped but not yet saved.\n")
        #expect(rig.body.isDirty)
        #expect(!(try rig.fixture.indexText(cardPath).contains("not yet saved")))

        #expect(rig.raw.enter())

        // The ordering, both halves. If the read had run first the editor would show a file the app
        // was a moment from overwriting from a buffer the user could no longer see.
        #expect(try rig.fixture.indexText(cardPath).contains("Typed but not yet saved."))
        #expect(rig.raw.text.contains("Typed but not yet saved."))
        #expect(rig.raw.text == (try rig.fixture.indexText(cardPath)), "the buffer is the file, byte for byte")
        #expect(!rig.body.isDirty)
    }

    @Test("Entering from Edit leaves the body column in Preview — and leaving raw source lands there")
    func rawSourceExitsToPreview() throws {
        let rig = try makeRig()
        defer { rig.fixture.tearDown() }
        rig.presentation.setMode(.edit)

        rig.raw.enter()

        // 05 lists "raw-source entry" among the three events that *leave Edit*, so entering genuinely
        // leaves it and Preview — the resting state — is what the exit reveals. Recorded as a choice
        // because 05 does not name the exit mode itself.
        #expect(rig.presentation.mode == .preview)
        rig.raw.cancel()
        #expect(rig.presentation.mode == .preview)
        #expect(!rig.raw.isActive)
    }

    @Test("An empty body after Apply does not drag the window into Edit")
    func theOpeningRuleDoesNotRunAgain() throws {
        let rig = try makeRig()
        defer { rig.fixture.tearDown() }
        // The window opened on a card with a body, so it opened in Preview — once, per the rule.
        #expect(rig.presentation.openIfNeeded(body: "# Notes\n") == .preview)
        rig.raw.enter()

        // The user deletes the body in source mode and applies.
        rig.raw.text = "---\nschema: 1\ntitle: Notes\norder: 1024\n---\n"
        #expect(rig.raw.applyAndLeave())

        // "The rule is about *opening* a card" (`CardBodyPresentation.openIfNeeded`), and a raw exit
        // is not an open: the user gets the blank Preview they just wrote, and ⌘E.
        #expect(rig.presentation.mode == .preview)
        #expect(try FrontmatterDocument.parse(rig.fixture.indexText(cardPath)).body.isEmpty)
    }

    @Test("Apply closes the outlet and lands the bytes")
    func applyCommitsAndLeaves() throws {
        let rig = try makeRig()
        defer { rig.fixture.tearDown() }
        rig.raw.enter()
        rig.raw.text = handEditedSource

        #expect(rig.raw.applyAndLeave())

        #expect(!rig.raw.isActive)
        #expect(rig.raw.alert == nil)
        #expect(try rig.fixture.indexData(cardPath) == Data(handEditedSource.utf8))
    }

    @Test("A failed validation keeps source mode open, with the alert and the text")
    func aFailedValidationStays() throws {
        let rig = try makeRig()
        defer { rig.fixture.tearDown() }
        let before = try rig.fixture.indexData(cardPath)
        rig.raw.enter()
        let broken = "---\nschema: 1\n  order: [unclosed\n---\nbody\n"
        rig.raw.text = broken

        #expect(!rig.raw.applyAndLeave())

        // "A failed validation keeps source mode open (toggle stays checked) with the alert" (05).
        #expect(rig.raw.isActive, "the toggle stays checked")
        #expect(rig.raw.text == broken, "and the user's text is still in front of them")
        guard case .invalid = rig.raw.alert else {
            Issue.record("expected a validation alert, got \(String(describing: rig.raw.alert))")
            return
        }
        #expect(try rig.fixture.indexData(cardPath) == before)

        // OK returns to editing the raw text — nothing else moves.
        rig.raw.dismissAlert()
        #expect(rig.raw.alert == nil)
        #expect(rig.raw.isActive)
        #expect(rig.raw.text == broken)
    }

    @Test("Cancel discards without ceremony — nothing written, nothing asked")
    func cancelDiscards() throws {
        let rig = try makeRig()
        defer { rig.fixture.tearDown() }
        let before = try rig.fixture.indexData(cardPath)
        rig.raw.enter()
        rig.raw.text = handEditedSource

        rig.raw.cancel()

        #expect(!rig.raw.isActive)
        #expect(rig.raw.applyAttempts == 0, "Cancel is not a write that happened to fail — it never asked")
        #expect(try rig.fixture.indexData(cardPath) == before)
        #expect(rig.raw.text.isEmpty, "and the buffer is gone with it, so a re-entry reads disk afresh")
    }

    @Test("A file that cannot be read does not open source mode")
    func anUnreadableFileDoesNotOpen() throws {
        let rig = try makeRig()
        defer { rig.fixture.tearDown() }
        var bytes = Data("---\nschema: 1\norder: 1024\ntitle: ".utf8)
        bytes.append(contentsOf: [0xFF, 0xFE])
        bytes.append(Data("\n---\nbody\n".utf8))
        try rig.fixture.item(cardPath, bytes: bytes)

        #expect(!rig.raw.enter())

        // Settled here, 05 being silent: an editor over lossily-decoded bytes would make the outlet's
        // one promise false, since Apply writes back whatever is in it.
        #expect(!rig.raw.isActive, "the toggle stays unchecked")
        guard case .unreadable = rig.raw.alert else {
            Issue.record("expected an unreadable alert, got \(String(describing: rig.raw.alert))")
            return
        }
    }

    @Test("A window that has not joined its board opens nothing and writes nothing")
    func unwiredSeamsFailClosed() {
        let raw = CardRawSourceSession()

        #expect(!raw.enter())
        #expect(!raw.isActive)
        #expect(!raw.applyAndLeave(), "and an Apply with no outlet open is not an Apply")
    }

    @Test("Re-entering after an Apply reads disk again rather than reusing the old buffer")
    func reEntryReadsFresh() throws {
        let rig = try makeRig()
        defer { rig.fixture.tearDown() }
        rig.raw.enter()
        rig.raw.text = handEditedSource
        rig.raw.applyAndLeave()

        // Somebody else — an agent, a pull — rewrites the card while no window holds it.
        let foreign = handEditedSource.replacingOccurrences(of: "Rewritten by hand.", with: "Rewritten by them.")
        try rig.fixture.item(cardPath, foreign)

        rig.raw.enter()
        #expect(rig.raw.text == foreign, "the outlet never shows an in-memory snapshot of the file")
    }
}

// MARK: - The menu rows

@MainActor
@Suite("Card view commands ▸ validation")
struct CardViewCommandValidationTests {

    @Test("Edit Body needs a card window, and disables while source mode is active")
    func editBodyDisablesUnderRawSource() {
        let presentation = CardBodyPresentation()
        let raw = CardRawSourceSession()
        raw.read = { .read("---\nschema: 1\norder: 1\n---\nbody\n") }
        raw.apply = { _ in .applied }

        // No card window in front: scope alone disables the row.
        #expect(!EditBodyCommand.isEnabled(body: nil, rawSource: nil))
        #expect(!EditBodyCommand.isEnabled(body: nil, rawSource: raw))
        // A card window, not in source mode: live.
        #expect(EditBodyCommand.isEnabled(body: presentation, rawSource: raw))

        raw.enter()

        // "View ▸ Edit Body (⌘E) disables while source mode is active, matching its toolbar item"
        // (05 ▸ Raw source outlet) — the two would otherwise be editing the same bytes from two
        // surfaces, one of which is not on screen.
        #expect(raw.isActive)
        #expect(!EditBodyCommand.isEnabled(body: presentation, rawSource: raw))

        raw.cancel()
        #expect(EditBodyCommand.isEnabled(body: presentation, rawSource: raw))
    }
}
