import Foundation
import Testing
@testable import Kanban

/// The inverses registered at the Writer boundary (13-native-undo.md ▸ Rules) — one round trip per
/// operation: perform the gesture, cross it backwards, read the **bytes on disk**, cross it forwards
/// again.
///
/// These drive a real store over a real temp board with a real `NativeHistoryProvider` behind it, and
/// assert against the files rather than the snapshot, like every other write suite here. That is the
/// only way to check the claim 13 actually makes: an undo is "an ordinary app-mediated write", not an
/// in-memory revert — so what has to come back is the *file*.
///
/// What "equals prior" means differs by operation, exactly as the design does: **byte-level for a
/// body** (the body span is all a body write touches) and **field-level for frontmatter** (`modified`
/// is stamped by every app write, undo included, so a byte comparison would be asserting the opposite
/// of the storage contract). `WriterFixture`, `Ident` and `Item` come from `WriterTestSupport.swift`.

// MARK: - Fixtures

/// A card sitting in `<root>/.trash/` — an ordinary card in a special place (03-board-ui.md §
/// Trash), with the unknown-key overlay every other fixture card carries so an inverse's
/// verbatim-preservation claim has something to preserve.
private func trashResident(order: String, title: String) -> String {
    """
    ---
    schema: 1
    title: \(title)
    order: \(order)
    project: lanework       # agent overlay
    created: 2026-01-01T09:00:00Z
    ---
    \(title) body.

    """
}

private let styledCard = """
---
schema: 1
title: Styled
order: 3072
project: lanework       # agent overlay
background: {color: blue}
icon: star
iconColor: chalk
created: 2026-01-01T09:00:00Z
---
Styled body.

"""

/// Two lanes — the first with two plain cards and a styled one, the second with one card — plus one
/// card already sitting in the board's `.trash/`. Enough for every inverse in this file.
@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("\(Ident.lane1)/\(Ident.card1)", Item.rich(order: "1024", title: "First"))
    try fixture.item("\(Ident.lane1)/\(Ident.card2)", Item.rich(order: "2048", title: "Second"))
    try fixture.item("\(Ident.lane1)/\(Ident.card3)", styledCard)
    try fixture.item(Ident.lane2, Item.rich(order: "2048", title: "Doing"))
    try fixture.item("\(Ident.lane2)/\(Ident.indexless)", Item.rich(order: "1024", title: "Elsewhere"))
    try fixture.item(".trash/\(Ident.card4)", trashResident(order: "1024", title: "Trashed"))
    return fixture
}

private let lane1 = ItemID(rawValue: Ident.lane1)
private let lane2 = ItemID(rawValue: Ident.lane2)
private let card1 = ItemID(rawValue: Ident.card1)
private let card2 = ItemID(rawValue: Ident.card2)
private let card3 = ItemID(rawValue: Ident.card3)
private let trashed = ItemID(rawValue: Ident.card4)
private let elsewhere = ItemID(rawValue: Ident.indexless)

private let card1Path = "\(Ident.lane1)/\(Ident.card1)"
private let card2Path = "\(Ident.lane1)/\(Ident.card2)"
private let card3Path = "\(Ident.lane1)/\(Ident.card3)"
private let trashedPath = ".trash/\(Ident.card4)"

/// A store with a stack behind it. The provider is returned because `BoardStore.history` is **weak**
/// — the session owns the stack in the app, and a test that dropped it would watch its own steps
/// disappear.
@MainActor
private func makeStore(_ fixture: WriterFixture) throws -> (store: BoardStore, history: NativeHistoryProvider) {
    let store = try BoardStore(rootURL: fixture.root)
    let history = NativeHistoryProvider()
    store.history = history
    return (store, history)
}

@MainActor
private func reload(_ store: BoardStore) async {
    store.handleWatcherEvent(.treeChanged(.appMediated))
    await store.awaitQuiescence()
}

/// One item's frontmatter as the app reads it — the level "equals prior" is asserted at for
/// everything but a body.
private func document(_ fixture: WriterFixture, _ relativePath: String) throws -> FrontmatterDocument {
    try FrontmatterDocument.parse(fixture.indexText(relativePath))
}

/// The file's lines minus the ones every app-mediated write owns — what an inverse must leave
/// byte-identical, unknown keys and their comments included.
private func untouchedLines(_ text: String) -> [Substring] {
    text.split(separator: "\n", omittingEmptySubsequences: false).filter {
        // `kind:` — the on-touch backfill rides every app rewrite of a file that lacks one, so a
        // round trip lands the same bytes *plus* the backfilled key, which is not the undo's doing.
        !$0.hasPrefix("modified") && !$0.hasPrefix("kind:")
    }
}

/// Every `index.md` on the board, keyed by its folder's path relative to the root — the whole of
/// what a round trip has to land back on, files-are-truth read literally.
///
/// Read as raw bytes off disk rather than through the loader, like every other assertion in this
/// file: the board being *equivalent* is not the claim, the board being the same bytes is.
private func boardTexts(_ fixture: WriterFixture) throws -> [String: String] {
    var texts: [String: String] = [:]
    let rootDepth = fixture.root.standardizedFileURL.pathComponents.count
    guard let walker = FileManager.default.enumerator(at: fixture.root, includingPropertiesForKeys: nil) else {
        return texts
    }
    for case let url as URL in walker where url.lastPathComponent == "index.md" {
        let folder = url.deletingLastPathComponent().standardizedFileURL
        let relative = folder.pathComponents.dropFirst(rootDepth).joined(separator: "/")
        texts[relative] = try String(decoding: Data(contentsOf: url), as: UTF8.self)
    }
    return texts
}

/// Asserts two whole-board readings are the same board — the same items, and every file identical
/// but for the stamps every app write owns.
///
/// **"Byte-identical" is byte-identical-except-`modified`**, and deliberately so: an inverse is an
/// ordinary app-mediated write (13-native-undo.md), so it stamps on the way past, and 13 promises
/// state equivalence rather than mtime equality. That is exactly the precision
/// `WriteFidelityTombstoneTests.deleteThenRestoreDiffersFromTheOriginalOnlyInTheModifiedTimestamp`
/// established for the delete→restore round trip, applied here to a whole board.
private func expectSameBoard(_ actual: [String: String], _ expected: [String: String], _ label: String) {
    #expect(actual.keys.sorted() == expected.keys.sorted(), "\(label): the board holds different items")
    for (path, text) in expected {
        guard let landed = actual[path] else { continue }
        #expect(untouchedLines(landed) == untouchedLines(text), "\(label): '\(path)' came back different")
    }
}

// MARK: - Resize

@MainActor
@Suite("Undo ▸ resize")
struct ResizeUndoTests {

    @Test("A width change undoes to the prior width and redoes to the new one")
    func widthRoundTrip() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        store.setLaneWidth(lane1, units: 3)
        #expect(try document(fixture, Ident.lane1).width.value == 3)
        #expect(history.undoActionName == "Resize Lane")

        history.undo()
        // The lane had no `width` key at all, so "prior" is its absence — not `width: 1`.
        #expect(try document(fixture, Ident.lane1).width.isMissing)

        history.redo()
        #expect(try document(fixture, Ident.lane1).width.value == 3)
    }

    @Test("An explicit prior width comes back as the value it was, not as the app's default")
    func priorValueIsRestoredVerbatim() async throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        store.setLaneWidth(lane1, units: 4)
        // The second gesture reads its prior value off the snapshot, so it has to see the first —
        // which is the one-way flow working, not a test artefact.
        await reload(store)

        store.setLaneWidth(lane1, units: 2)
        #expect(try document(fixture, Ident.lane1).width.value == 2)

        history.undo()
        #expect(try document(fixture, Ident.lane1).width.value == 4)
    }

    @Test("A multi-lane step is one step with a plural title")
    func batchIsOneStep() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        store.stepLaneWidths([lane1, lane2], by: 1)

        #expect(history.undoActionName == "Resize 2 Lanes")
        #expect(try document(fixture, Ident.lane1).width.value == 2)
        #expect(try document(fixture, Ident.lane2).width.value == 2)

        history.undo()
        #expect(try document(fixture, Ident.lane1).width.isMissing)
        #expect(try document(fixture, Ident.lane2).width.isMissing)
        #expect(history.canUndo == false, "one gesture, one step")
    }
}

// MARK: - Restyle

@MainActor
@Suite("Undo ▸ restyle")
struct RestyleUndoTests {

    @Test("A style change undoes to the prior values — removing the keys that were not there")
    func styleRoundTrip() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)
        let before = try fixture.indexText(card1Path)

        store.applyStyle(to: .items([card1]), background: .set("red"), icon: .set("flag"))
        #expect(try document(fixture, card1Path).background.value == "red")
        #expect(history.undoActionName == "Restyle Card")

        history.undo()
        let undone = try document(fixture, card1Path)
        #expect(undone.background.isMissing)
        #expect(undone.icon.isMissing)
        // Nothing but the styled keys and the stamp moved — the unknown key with its comment, the
        // reserved `labels`, `created`, and the body all came back through untouched.
        #expect(untouchedLines(try fixture.indexText(card1Path)) == untouchedLines(before))

        history.redo()
        #expect(try document(fixture, card1Path).background.value == "red")
        #expect(try document(fixture, card1Path).icon.value == "flag")
    }

    @Test("A prior value is restored as itself, not removed")
    func priorStyleValueComesBack() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        store.applyStyle(to: .items([card3]), background: .set("green"))

        history.undo()
        let undone = try document(fixture, card3Path)
        #expect(undone.background.value == "blue")
        #expect(undone.icon.value == "star", "a dimension the gesture did not touch is not touched back")
        #expect(undone.iconColor.value == "chalk", "the third dimension rides the same rule")
    }

    @Test("The colour row's dimension round-trips: a tint undoes to the prior tint, and to absence where there was none")
    func iconColorRoundTrip() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        // card3 carries `iconColor: chalk`; card1 carries none.
        store.applyStyle(to: .items([card3]), iconColor: .set("fern"))
        #expect(try document(fixture, card3Path).iconColor.value == "fern")

        history.undo()
        #expect(try document(fixture, card3Path).iconColor.value == "chalk")
        history.redo()
        #expect(try document(fixture, card3Path).iconColor.value == "fern")

        store.applyStyle(to: .items([card1]), iconColor: .set("carnation"))
        history.undo()
        #expect(try document(fixture, card1Path).iconColor.isMissing, "no prior tint undoes to no key at all")
    }

    @Test("A styling batch is one step, with a plural title")
    func batchIsOneStep() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        store.applyStyle(to: .items([card1, card2, card3]), background: .set("red"))

        #expect(history.undoActionName == "Restyle 3 Cards")
        history.undo()
        #expect(try document(fixture, card1Path).background.isMissing)
        #expect(try document(fixture, card2Path).background.isMissing)
        #expect(try document(fixture, card3Path).background.value == "blue")
        #expect(history.canUndo == false)
    }

    @Test("The board's own styling names the board")
    func boardStyleIsNamedForTheBoard() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        store.applyStyle(to: .board, background: .set("graphite"))

        #expect(history.undoActionName == "Restyle Board")
        history.undo()
        #expect(try document(fixture, "").background.isMissing)
    }
}

// MARK: - Rename

@MainActor
@Suite("Undo ▸ rename")
struct RenameUndoTests {

    @Test("A card rename undoes to the prior title and redoes to the new one")
    func renameRoundTrip() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        store.transient.beginRename(of: card1, currentTitle: "First")
        store.transient.updateRenameDraft("Renamed")
        store.commitRename()

        #expect(try document(fixture, card1Path).title.value == "Renamed")
        #expect(history.undoActionName == "Rename Card")

        history.undo()
        #expect(try document(fixture, card1Path).title.value == "First")

        history.redo()
        #expect(try document(fixture, card1Path).title.value == "Renamed")
    }

    @Test("An emptied title undoes back to the title that was there")
    func emptyRenameRoundTrip() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        store.transient.beginRename(of: card1, currentTitle: "First")
        store.transient.updateRenameDraft("   ")
        store.commitRename()
        #expect(try document(fixture, card1Path).title.isMissing, "an empty commit removes the key")

        history.undo()
        #expect(try document(fixture, card1Path).title.value == "First")

        history.redo()
        #expect(try document(fixture, card1Path).title.isMissing)
    }

    @Test("A lane rename is named for the lane; the board's for the board")
    func namesFollowTheLevel() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        store.transient.beginRename(of: lane1, currentTitle: "Todo")
        store.transient.updateRenameDraft("Backlog")
        store.commitRename()
        #expect(history.undoActionName == "Rename Lane")

        store.renameBoard("Project")
        #expect(history.undoActionName == "Rename Board")
        #expect(try document(fixture, "").title.value == "Project")

        history.undo()
        #expect(try document(fixture, "").title.value == "Board")
        history.undo()
        #expect(try document(fixture, Ident.lane1).title.value == "Todo")
    }

    @Test("An unchanged rename writes nothing and registers nothing")
    func aNoOpRenameRegistersNothing() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        store.transient.beginRename(of: card1, currentTitle: "First")
        store.transient.updateRenameDraft("First")
        store.commitRename()

        #expect(history.canUndo == false)
    }
}

// MARK: - Create

@MainActor
@Suite("Undo ▸ create")
struct CreateUndoTests {

    @Test("Undoing a lane create removes the folder; redo puts it back, identity and bytes intact")
    func laneCreateRoundTrip() async throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)
        let before = try fixture.entryNames("")

        store.createLane()
        let created = try #require(try fixture.entryNames("").first { !before.contains($0) })
        let bytes = try fixture.indexData(created)
        #expect(history.undoActionName == "Add Lane")

        history.undo()
        #expect(fixture.exists(created) == false, "an undone create leaves no trace — not a tombstone")

        history.redo()
        #expect(fixture.exists(created), "the same UUID, so every later step still names something")
        #expect(try fixture.indexData(created) == bytes, "replayed verbatim — nothing re-serialized")
    }

    @Test("Undoing a card create removes the folder, rank and all")
    func cardCreateRoundTrip() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)
        let before = try fixture.entryNames(Ident.lane1)

        store.transient.beginPlaceholder(inLane: lane1, after: card1)
        store.transient.updateDraft("Fresh")
        let created = try #require(store.commitPlaceholder())
        let path = "\(Ident.lane1)/\(created.rawValue)"
        let bytes = try fixture.indexData(path)
        #expect(history.undoActionName == "Add Card")
        #expect(try fixture.entryNames(Ident.lane1).count == before.count + 1)

        history.undo()
        #expect(fixture.exists(path) == false)

        history.redo()
        #expect(try fixture.indexData(path) == bytes, "the rank it was placed at rides in its bytes")
    }
}

// MARK: - Move and reorder

@MainActor
@Suite("Undo ▸ move and reorder")
struct MoveUndoTests {

    @Test("A cross-lane move undoes to the original lane at the original order")
    func moveRoundTrip() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        store.moveCards([card1], toLane: lane2, at: 0)
        #expect(fixture.exists("\(Ident.lane2)/\(Ident.card1)"))
        #expect(fixture.exists(card1Path) == false)
        #expect(history.undoActionName == "Move Card")

        history.undo()
        #expect(fixture.exists(card1Path), "the folder moved back")
        #expect(fixture.exists("\(Ident.lane2)/\(Ident.card1)") == false)
        #expect(try document(fixture, card1Path).order.value == 1024, "at the rank it left")

        history.redo()
        #expect(fixture.exists("\(Ident.lane2)/\(Ident.card1)"))
    }

    @Test("A multi-card move is one step with a plural title")
    func multiCardMoveIsOneStep() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        store.moveCards([card1, card2, card3], toLane: lane2, at: 0)

        #expect(history.undoActionName == "Move 3 Cards")
        history.undo()
        #expect(fixture.exists(card1Path))
        #expect(fixture.exists(card2Path))
        #expect(fixture.exists(card3Path))
        #expect(try document(fixture, card1Path).order.value == 1024)
        #expect(try document(fixture, card2Path).order.value == 2048)
        #expect(try document(fixture, card3Path).order.value == 3072)
        #expect(history.canUndo == false, "one gesture, one step")
    }

    @Test("A drop that stays in its own lane is a Reorder, and undoes to the rank it held")
    func sameLaneDropIsAReorder() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        store.moveCards([card1], toLane: lane1, at: 2)

        #expect(history.undoActionName == "Reorder Card")
        #expect(try document(fixture, card1Path).order.value != 1024)

        history.undo()
        #expect(try document(fixture, card1Path).order.value == 1024)
    }

    @Test("A lane drag undoes to the lane's own prior rank")
    func laneReorderRoundTrip() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        store.moveLane(lane1, toIndex: 1)
        let moved = try #require(try document(fixture, Ident.lane1).order.value)
        #expect(moved > 2048)
        #expect(history.undoActionName == "Reorder Lane")

        history.undo()
        #expect(try document(fixture, Ident.lane1).order.value == 1024)

        history.redo()
        #expect(try document(fixture, Ident.lane1).order.value == moved)
    }

    /// **The inverses conform to the container-change predicate** (01-storage-format.md
    /// § Frontmatter ▸ `modified`'s scope, refined 2026-07-30) — the m8 conformance check, stated at
    /// the level the rule is about: an inverse is an ordinary app-mediated write, so it is subject to
    /// the *same* predicate as the gesture it inverts, not to a rule of its own.
    ///
    /// Three claims in one round trip, because they are one claim: the undo of a within-lane reorder is
    /// itself a within-lane reorder and rewrites only `order`; the undo of a cross-lane move is itself a
    /// cross-lane move and stamps; and **no trash-specific branch exists in either direction** — the
    /// trash round trip stamps for the same reason the cross-lane one does.
    ///
    /// It reads `modified-by` rather than `modified`, deliberately: `untouchedLines` filters the whole
    /// `modified*` family precisely because a content write is *expected* to move it, so the foreign
    /// stamp's survival is the assertion with a sharp edge — it survives an order-only rewrite and is
    /// cleared by a content one, and `Item.rich` plants one on every fixture card for exactly this.
    @Test("An inverse stamps only when it changes a container")
    func inversesFollowTheContainerPredicate() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        // Within-lane, there and back: nothing on either leg is a content write.
        store.moveCards([card1], toLane: lane1, at: 2)
        #expect(try document(fixture, card1Path).rawValue(for: FrontmatterKeys.modifiedBy) == "claude")
        history.undo()
        #expect(
            try document(fixture, card1Path).rawValue(for: FrontmatterKeys.modifiedBy) == "claude",
            "undoing a reorder is a reorder — order-only, both ways"
        )

        // Cross-lane, there and back: both legs change the container, so both stamp.
        store.moveCards([card2], toLane: lane2, at: 0)
        #expect(try document(fixture, "\(Ident.lane2)/\(Ident.card2)").rawValue(for: FrontmatterKeys.modifiedBy) == nil)
        // Re-planted by hand, standing in for an agent that stamped the card in its new lane — the
        // inverse has to clear it again, because moving back is itself a container change.
        try BoardWriter.updateIndex(
            inItemFolder: fixture.url("\(Ident.lane2)/\(Ident.card2)"), operation: .style(title: nil)
        ) { $0.set(FrontmatterKeys.modifiedBy, to: .string("claude")) }
        history.undo()
        #expect(
            try document(fixture, card2Path).rawValue(for: FrontmatterKeys.modifiedBy) == nil,
            "undoing a cross-lane move is a cross-lane move — it stamps"
        )
    }

    /// The lane half of the same claim: a lane's container is the board root and never changes, so a
    /// lane drag and its inverse are both order-only.
    @Test("A lane reorder and its inverse are both order-only")
    func laneInversesAreOrderOnly() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        store.moveLane(lane1, toIndex: 1)
        #expect(try document(fixture, Ident.lane1).rawValue(for: FrontmatterKeys.modifiedBy) == "claude")
        history.undo()
        #expect(try document(fixture, Ident.lane1).rawValue(for: FrontmatterKeys.modifiedBy) == "claude")
        history.redo()
        #expect(try document(fixture, Ident.lane1).rawValue(for: FrontmatterKeys.modifiedBy) == "claude")
    }

    /// The trash round trip, from the undo stack rather than the Writer: the delete stamps and its
    /// inverse — the move back out — stamps too. **Neither is a special case**; both are container
    /// changes, which is the whole of the refinement.
    @Test("A delete and its inverse both stamp, with no trash branch")
    func theTrashRoundTripStampsBothWays() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        store.select([card1], in: .board)
        store.deleteSelection()
        #expect(fixture.exists(".trash/\(Ident.card1)"))
        #expect(try document(fixture, ".trash/\(Ident.card1)").rawValue(for: FrontmatterKeys.modifiedBy) == nil)

        try BoardWriter.updateIndex(
            inItemFolder: fixture.url(".trash/\(Ident.card1)"), operation: .style(title: nil)
        ) { $0.set(FrontmatterKeys.modifiedBy, to: .string("claude")) }
        history.undo()
        #expect(fixture.exists(card1Path))
        #expect(
            try document(fixture, card1Path).rawValue(for: FrontmatterKeys.modifiedBy) == nil,
            "restoring out of the trash is a container change and clears the stamp"
        )
    }

    @Test("⌥⌘↓ undoes the whole permutation, siblings included")
    func sortRoundTrip() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)
        store.select([card1], in: .board)

        store.sortSelection(.down)

        #expect(history.undoActionName == "Reorder Card")
        #expect(try document(fixture, card1Path).order.value == 2048)
        #expect(try document(fixture, card2Path).order.value == 1024)

        history.undo()
        #expect(try document(fixture, card1Path).order.value == 1024)
        #expect(try document(fixture, card2Path).order.value == 2048)

        history.redo()
        #expect(try document(fixture, card1Path).order.value == 2048)
        #expect(try document(fixture, card2Path).order.value == 1024)
    }
}

// MARK: - Delete

@MainActor
@Suite("Undo ▸ delete")
struct TrashUndoTests {

    @Test("Undoing a card delete moves it back out of the trash, to its lane and its rank")
    func deleteRoundTrip() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)
        let before = try fixture.indexText(card1Path)

        store.delete([card1])
        #expect(!fixture.exists(card1Path), "the folder physically left its lane")
        #expect(fixture.exists(".trash/\(Ident.card1)"))
        #expect(history.undoActionName == "Delete Card")

        history.undo()
        #expect(fixture.exists(card1Path), "13 ▸ Interaction with the trash: the undo is the move back")
        #expect(!fixture.exists(".trash/\(Ident.card1)"))
        let undone = try fixture.indexText(card1Path)
        #expect(try FrontmatterDocument.parse(undone).order.value == 1024, "at its original rank")
        // Byte-identical but for the stamps every app write owns — no `deleted:` key was ever
        // written, so there is none to come back and none to remove.
        #expect(untouchedLines(undone) == untouchedLines(before))

        history.redo()
        #expect(fixture.exists(".trash/\(Ident.card1)"))
        #expect(!fixture.exists(card1Path))
    }

    /// **There is no rank to capture or replay** (01-storage-format.md § Deletion, re-ruled
    /// 2026-07-31): a delete is a folder move plus a `modified` stamp, so the card's `order` rides
    /// along untouched through delete, undo and redo alike, and the redo is just the forward write
    /// run again.
    @Test("The redo re-runs the delete, and `order` is untouched at every leg")
    func redoRerunsTheDelete() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        store.delete([card1])
        #expect(try document(fixture, ".trash/\(Ident.card1)").order.value == 1024,
                "the lane rank rode along; nothing was minted")

        history.undo()
        #expect(try document(fixture, card1Path).order.value == 1024)
        history.redo()

        #expect(try document(fixture, ".trash/\(Ident.card1)").order.value == 1024)
        // And the redone delete restamps, which is what puts it back on top of the column.
        #expect(try loadedTrash(fixture).map(\.id).first == card1)
    }

    @Test("A multi-card delete is one step with a plural title, and the run lands on top")
    func batchDeleteIsOneStep() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        store.delete([card1, card2])

        #expect(history.undoActionName == "Delete 2 Cards")
        // Both cards keep the ranks they had in their lane — nothing is minted, at either end of the
        // run — and both land above the board's existing resident by their fresh stamps.
        #expect(try document(fixture, ".trash/\(Ident.card1)").order.value == 1024)
        #expect(try document(fixture, ".trash/\(Ident.card2)").order.value == 2048)
        #expect(try loadedTrash(fixture).map(\.id).suffix(1) == [trashed])

        history.undo()
        #expect(fixture.exists(card1Path))
        #expect(fixture.exists(card2Path))
        #expect(try loadedTrash(fixture).map(\.id) == [trashed], "only the board's own resident is left")
        #expect(history.canUndo == false)
    }

    /// 13 ▸ Interaction with the trash: "a lane [returns] to its strip position (subtree intact — it
    /// never left the folder)". The inverse is the ordinary move back, so the capture/recreate
    /// machinery is retired: the lane's bytes never left the disk to need replaying.
    @Test("A lane delete is a move into the trash, and its undo is the move back to its strip rank")
    func laneDeleteRoundTrip() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)
        let laneBefore = try fixture.indexText(Ident.lane2)
        let cardText = try fixture.indexText("\(Ident.lane2)/\(Ident.indexless)")
        let laneRank = try #require(try document(fixture, Ident.lane2).order.value)

        store.delete([lane2])

        #expect(!fixture.exists(Ident.lane2))
        #expect(fixture.exists(".trash/\(Ident.lane2)"))
        #expect(try loadedTrash(fixture).map(\.id) == [trashed], "its cards are not trash cards")
        #expect(try loadedTrashedLanes(fixture).map(\.id) == [lane2])
        #expect(history.undoActionName == "Delete Lane")
        #expect(try document(fixture, ".trash/\(Ident.lane2)").order.value == laneRank,
                "the strip rank rides along; no trash rank is minted")

        history.undo()

        #expect(fixture.exists(Ident.lane2))
        #expect(!fixture.exists(".trash/\(Ident.lane2)"))
        #expect(try document(fixture, Ident.lane2).order.value == laneRank, "at its own strip position")
        #expect(untouchedLines(try fixture.indexText(Ident.lane2)) == untouchedLines(laneBefore),
                "byte-identical but for the stamps every container-changing move owns")
        #expect(try fixture.indexText("\(Ident.lane2)/\(Ident.indexless)") == cardText,
                "the subtree never moved relative to its lane, so not one nested byte changed")

        history.redo()
        #expect(fixture.exists(".trash/\(Ident.lane2)"))
        #expect(try document(fixture, ".trash/\(Ident.lane2)").order.value == laneRank,
                "the redo re-runs the forward write, which has no rank to replay")
    }

    @Test("A multi-lane delete is one step with a plural title")
    func batchLaneDeleteIsOneStep() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        store.delete([lane1, lane2])

        #expect(history.undoActionName == "Delete 2 Lanes")
        // Their strip ranks, untouched — a lane delete mints nothing either.
        #expect(try document(fixture, ".trash/\(Ident.lane1)").order.value == 1024)
        #expect(try document(fixture, ".trash/\(Ident.lane2)").order.value == 2048)

        history.undo()
        #expect(fixture.exists(Ident.lane1))
        #expect(fixture.exists(Ident.lane2))
        #expect(try loadedTrashedLanes(fixture).isEmpty)
        #expect(history.canUndo == false)
    }

    @Test("A restore-by-move-out registers as an ordinary Move, with the ordinary move inverse")
    func restoreIsAnOrdinaryMove() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)
        // The `order` the row was *carrying* while trashed — its old lane rank, which the trash move
        // never rewrote and which this restore is about to overwrite.
        let carriedOrder = try #require(try document(fixture, trashedPath).order.value)

        store.moveCards([trashed], toLane: lane2, at: 0)

        #expect(fixture.exists("\(Ident.lane2)/\(Ident.card4)"))
        #expect(!fixture.exists(trashedPath))
        #expect(history.undoActionName == "Move Card",
                "13: a restore is an ordinary move between containers, named as one")

        history.undo()
        #expect(fixture.exists(trashedPath), "back in the trash it came out of")
        #expect(!fixture.exists("\(Ident.lane2)/\(Ident.card4)"))
        #expect(try document(fixture, trashedPath).order.value == carriedOrder,
                "the undo puts back the rank the restore overwrote")

        history.redo()
        #expect(fixture.exists("\(Ident.lane2)/\(Ident.card4)"))
    }
}

/// The board's trash as the loader reads it — never the store's snapshot, which a write deliberately
/// does not touch.
@MainActor
private func loadedTrash(_ fixture: WriterFixture) throws -> [Card] {
    try BoardLoader.load(boardRoot: fixture.root).model.trash
}

@MainActor
private func loadedTrashedLanes(_ fixture: WriterFixture) throws -> [TrashedLane] {
    try BoardLoader.load(boardRoot: fixture.root).model.trashedLanes
}

// MARK: - The Edit session

@MainActor
@Suite("Undo ▸ the Edit session")
struct BodyUndoTests {

    @Test("A session's saves are one step, registered at the flip, with the body it started from")
    func sessionIsOneStep() async throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)
        let original = try FrontmatterDocument.parse(fixture.indexText(card1Path)).body

        let session = CardBodyEditSession()
        session.save = { [weak store] text in store?.writeCardBody(inCard: card1, body: text) ?? .vanished }
        session.registerUndo = { [weak store] prior, new in
            store?.registerBodyEdit(inCard: card1, priorBody: prior, newBody: new)
        }
        session.adopt(diskBody: original)

        // Three debounced ticks inside one session — none of them a step.
        session.edited("One.\n")
        _ = session.flush()
        session.edited("One two.\n")
        _ = session.flush()
        session.edited("One two three.\n")
        _ = session.flush()
        #expect(history.canUndo == false, "a save tick is not a step")

        session.endEditSession()
        #expect(history.undoActionName == "Edit Card")

        history.undo()
        #expect(try FrontmatterDocument.parse(fixture.indexText(card1Path)).body == original,
                "byte-for-byte, across every tick the session made")

        history.redo()
        #expect(try FrontmatterDocument.parse(fixture.indexText(card1Path)).body == "One two three.\n")
    }

    @Test("A session that only read registers nothing")
    func anUntouchedSessionRegistersNothing() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)
        let original = try FrontmatterDocument.parse(fixture.indexText(card1Path)).body

        let session = CardBodyEditSession()
        session.save = { [weak store] text in store?.writeCardBody(inCard: card1, body: text) ?? .vanished }
        session.registerUndo = { [weak store] prior, new in
            store?.registerBodyEdit(inCard: card1, priorBody: prior, newBody: new)
        }
        session.adopt(diskBody: original)

        session.endEditSession()

        #expect(history.canUndo == false)
    }

    @Test("A session typed back to where it started registers nothing")
    func aRevertedSessionRegistersNothing() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)
        let original = try FrontmatterDocument.parse(fixture.indexText(card1Path)).body

        let session = CardBodyEditSession()
        session.save = { [weak store] text in store?.writeCardBody(inCard: card1, body: text) ?? .vanished }
        session.registerUndo = { [weak store] prior, new in
            store?.registerBodyEdit(inCard: card1, priorBody: prior, newBody: new)
        }
        session.adopt(diskBody: original)

        session.edited("Something else.\n")
        _ = session.flush()
        session.edited(original)
        _ = session.flush()
        session.endEditSession()

        #expect(history.canUndo == false, "the net effect on the file is nothing to undo")
        #expect(try FrontmatterDocument.parse(fixture.indexText(card1Path)).body == original)
    }
}

// MARK: - What registers nothing

@MainActor
@Suite("Undo ▸ the operations that register nothing")
struct NotUndoableTests {

    @Test("The trash's permanent delete registers nothing — the confirm is the safety")
    func purgeRegistersNothing() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)
        store.delete([card1])
        let armed = try #require(history.undoActionName)

        store.select([trashed], in: .trash)
        store.deleteTrashEntries([trashed])

        #expect(fixture.exists(trashedPath) == false)
        #expect(store.purgeIsUnrecoverable)
        #expect(history.undoActionName == armed, "the stack is exactly where the purge found it")
    }

    @Test("Empty Trash registers nothing either")
    func emptyTrashRegistersNothing() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        store.emptyTrash()

        #expect(fixture.exists(trashedPath) == false)
        #expect(history.canUndo == false)
    }

    @Test("Attachment add and remove register nothing in v1")
    func attachmentsRegisterNothing() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)
        let source = try fixture.file("outside/note.txt", Data("hello".utf8))

        store.importAttachments([source], toCard: card1)
        #expect(fixture.exists("\(card1Path)/attachments") )
        #expect(history.canUndo == false)

        store.removeAttachment(named: "note.txt", fromCard: card1)
        #expect(history.canUndo == false)
    }

    @Test("A checkbox toggle and a raw-source Apply are outside 13's inventory")
    func bodyAdjacentWritesRegisterNothing() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        _ = store.applyCardSource(inCard: card1, text: "---\nschema: 1\norder: 1024\n---\nApplied.\n")

        #expect(try FrontmatterDocument.parse(fixture.indexText(card1Path)).body == "Applied.\n")
        #expect(history.canUndo == false)
    }

    @Test("A store with no stack behind it writes exactly as it always did")
    func aStorelessBoardStillWrites() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let store = try BoardStore(rootURL: fixture.root)

        store.delete([card1])
        store.setLaneWidth(lane1, units: 2)

        #expect(fixture.exists(".trash/\(Ident.card1)"))
        #expect(try document(fixture, Ident.lane1).width.value == 2)
        #expect(store.banners.oneShots.isEmpty)
    }
}

// MARK: - The crossing is a write

@MainActor
@Suite("Undo ▸ crossings are ordinary writes")
struct CrossingIsAWriteTests {

    @Test("An undo goes through the Writer: it stamps, and it echoes back through the reload")
    func undoIsAnAppMediatedWrite() async throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        store.transient.beginRename(of: card1, currentTitle: "First")
        store.transient.updateRenameDraft("Renamed")
        store.commitRename()

        history.undo()

        let undone = try document(fixture, card1Path)
        #expect(undone.title.value == "First")
        let stamped = try #require(undone.modified.value)
        #expect(stamped.timeIntervalSinceNow > -30, "an inverse is a real write, not an in-memory revert")
        #expect(try fixture.indexText(card1Path).contains("modified-by") == false)

        // And the board sees it the only way it ever sees anything: through a reload.
        await reload(store)
        let card = try #require(store.snapshot.lanes.first?.cards.first { $0.id == card1 })
        #expect(card.title.value == "First")
    }

    @Test("Undo, redo, undo — the classic dance over one file")
    func theDanceRepeats() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        store.delete([card1])
        for _ in 0 ..< 3 {
            history.undo()
            #expect(fixture.exists(card1Path))
            history.redo()
            #expect(fixture.exists(".trash/\(Ident.card1)"))
        }
    }
}

// MARK: - Staleness

/// A writer that is not the app: `BoardWriter` reached **around** the store, which is exactly what an
/// agent, a hand edit or another editor is from the stack's point of view — a change no step was
/// registered for (13-native-undo.md ▸ Rules: "Foreign writes never join the stack ... collisions are
/// handled lazily, per step, by validation").
private enum Foreign {

    static func rename(_ fixture: WriterFixture, _ path: String, to title: String) throws {
        try BoardWriter.updateIndex(inItemFolder: fixture.url(path), operation: .rename(title: nil)) { document in
            document.set(FrontmatterKeys.title, to: .string(title))
        }
    }

    static func restyle(_ fixture: WriterFixture, _ path: String, background: String) throws {
        try BoardWriter.updateIndex(inItemFolder: fixture.url(path), operation: .style(title: nil)) { document in
            document.setStyleValue(background, for: FrontmatterKeys.background)
        }
    }

    static func setOrder(_ fixture: WriterFixture, _ path: String, to order: Double) throws {
        try BoardWriter.updateIndex(inItemFolder: fixture.url(path), operation: .reorder(title: nil)) { document in
            document.set(FrontmatterKeys.order, to: .double(order))
        }
    }

    static func setBody(_ fixture: WriterFixture, _ path: String, to body: String) throws {
        _ = try BoardWriter.writeBody(inItemFolder: fixture.url(path), body: body)
    }

    /// A foreign delete of a **card** — the shape a delete has on disk now: the folder moves into
    /// `.trash/`. `FileManager` and nothing else, so no rank is minted and no stamp is written.
    static func trash(_ fixture: WriterFixture, _ path: String, id: String) throws {
        try fixture.move(path, toTrash: id)
    }

    /// A **Finder deletion** of a lane: the folder disappears entirely, which "is also a delete"
    /// (01 § Deletion) — such items never enter the trash, so this is the shape a foreign writer's
    /// destructive removal has, distinct from the app's own delete (a move into `.trash/`).
    static func removeLane(_ fixture: WriterFixture, _ path: String) throws {
        try FileManager.default.removeItem(at: fixture.url(path))
    }

    static func purge(_ fixture: WriterFixture, _ path: String) throws {
        try BoardWriter.purgeItem(at: fixture.url(path))
    }

    static func move(_ fixture: WriterFixture, _ path: String, toLane lane: String, order: Double) throws {
        _ = try BoardWriter.moveItem(
            at: fixture.url(path),
            toParent: fixture.url(lane),
            sourceBoardRoot: fixture.root,
            destinationBoardRoot: fixture.root,
            order: order
        )
    }
}

/// The staleness predicate at ⌘Z time (13-native-undo.md ▸ Rules ▸ staleness validation): "an inverse
/// operation re-checks its target against the current snapshot at ⌘Z time ... Target folder gone, or
/// the field no longer holding the step's after-value → the step is **skipped, not applied**: popped
/// from the stack with an info-tone banner ... and ⌘Z falls through to the next step."
///
/// Every test here is the same hostile shape: perform a gesture, let somebody else write to the board
/// behind the app's back, then press ⌘Z and read the **file**. What must never happen is the inverse
/// landing on top of the foreign write.
@MainActor
@Suite("Undo ▸ staleness")
struct StaleStepTests {

    // MARK: Existence and liveness

    @Test("A foreign delete of the target skips its step — and ⌘Z falls through to the next one")
    func aForeignDeleteSkipsAndFallsThrough() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        store.transient.beginRename(of: card1, currentTitle: "First")
        store.transient.updateRenameDraft("Renamed")
        store.commitRename()
        store.transient.beginRename(of: card2, currentTitle: "Second")
        store.transient.updateRenameDraft("Second!")
        store.commitRename()

        try Foreign.trash(fixture, card2Path, id: Ident.card2)
        history.undo()

        // The top step's card is in the trash now, so its rename is not ours to walk back; the one
        // below it is untouched and applies in the same ⌘Z.
        #expect(try document(fixture, ".trash/\(Ident.card2)").title.value == "Second!",
                "the foreign writer's board, left alone")
        #expect(try document(fixture, card1Path).title.value == "First", "⌘Z fell through and did something")
        #expect(store.banners.signposts.map(\.message)
            == ["Undo skipped — 'Second!' changed outside Lanework"])
        #expect(history.canUndo == false, "both steps were consumed — one skipped, one applied")
        #expect(history.redoActionName == "Rename Card", "only the step that ran is redoable")
    }

    @Test("A target the foreign writer removed outright skips rather than failing")
    func aVanishedTargetSkips() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        store.delete([card1])
        try Foreign.purge(fixture, ".trash/\(Ident.card1)")

        history.undo()

        #expect(fixture.exists(card1Path) == false)
        #expect(!fixture.exists(".trash/\(Ident.card1)"))
        #expect(store.banners.signposts.count == 1)
        #expect(store.banners.oneShots.isEmpty, "a skip is not a write failure — no error row")
        #expect(history.canUndo == false)
        #expect(history.canRedo == false, "a skipped step leaves nothing behind")
    }

    /// The container check, and it needs no field of its own: a delete step's undo expects its card
    /// at `<root>/.trash/<id>`, and a foreign restore leaves that path empty (`HistoryStaleness`).
    @Test("A foreign restore skips the delete's undo — the card is not in the container we left it")
    func aForeignRestoreSkipsTheDeleteStep() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        store.delete([card1])
        try fixture.move(".trash/\(Ident.card1)", toLane: Ident.lane2, card: Ident.card1)

        history.undo()

        #expect(fixture.exists("\(Ident.lane2)/\(Ident.card1)"), "where the foreign writer put it")
        #expect(!fixture.exists(card1Path), "and not moved back on top of them")
        #expect(store.banners.signposts.map(\.message) == ["Undo skipped — 'First' changed outside Lanework"])
    }

    @Test("A card a foreign writer moved away leaves nothing at the destination to walk back")
    func aForeignMoveSkipsTheMoveStep() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        store.moveCards([card1], toLane: lane2, at: 0)
        try Foreign.move(fixture, "\(Ident.lane2)/\(Ident.card1)", toLane: Ident.lane1, order: 5000)

        history.undo()

        #expect(fixture.exists(card1Path), "where the foreign writer put it")
        #expect(try document(fixture, card1Path).order.value == 5000, "at the rank they gave it, not ours")
        #expect(store.banners.signposts.count == 1)
        #expect(history.canUndo == false)
    }

    @Test("A foreign lane delete is stale for a field edit — there is nothing at the path")
    func aRemovedLaneIsStaleForAFieldEdit() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        store.setLaneWidth(lane1, units: 3)
        try Foreign.removeLane(fixture, Ident.lane1)

        history.undo()

        #expect(!fixture.exists(Ident.lane1), "not conjured back to be resized")
        #expect(store.banners.signposts.count == 1)
    }

    /// The card's own path is the check: its lane's folder is gone, so the card is simply not there
    /// any more — no ancestor walk, which is what materializing the trash bought. (The same holds
    /// when the app trashes the lane: the card's path moves under `.trash/` and nothing is left at
    /// the one the step named.)
    @Test("A card whose lane a foreign writer deleted is stale too")
    func aCardUnderARemovedLaneIsStale() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        store.transient.beginRename(of: card1, currentTitle: "First")
        store.transient.updateRenameDraft("Renamed")
        store.commitRename()
        try Foreign.removeLane(fixture, Ident.lane1)

        history.undo()

        #expect(!fixture.exists(card1Path), "the card is gone; nothing was written")
        #expect(store.banners.signposts.count == 1)
    }

    // MARK: The field-level predicate

    @Test("A foreign edit of the very field the step wrote skips it")
    func aForeignFieldEditSkips() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        store.transient.beginRename(of: card1, currentTitle: "First")
        store.transient.updateRenameDraft("Renamed")
        store.commitRename()

        try Foreign.rename(fixture, card1Path, to: "Theirs")
        history.undo()

        #expect(try document(fixture, card1Path).title.value == "Theirs",
                "never apply a stale inverse on top of someone else's newer write")
        #expect(store.banners.signposts.map(\.message) == ["Undo skipped — 'Renamed' changed outside Lanework"])
        #expect(history.canUndo == false)
        #expect(history.canRedo == false)
    }

    @Test("A foreign change to an unrelated item skips nothing — the predicate is per target")
    func anUnrelatedForeignChangeAppliesNormally() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        store.transient.beginRename(of: card1, currentTitle: "First")
        store.transient.updateRenameDraft("Renamed")
        store.commitRename()

        try Foreign.rename(fixture, card2Path, to: "Theirs")
        try Foreign.trash(fixture, card3Path, id: Ident.card3)
        history.undo()

        #expect(try document(fixture, card1Path).title.value == "First", "the step applied")
        #expect(try document(fixture, card2Path).title.value == "Theirs", "and left the neighbours alone")
        #expect(store.banners.signposts.isEmpty, "nothing to explain")
        #expect(history.redoActionName == "Rename Card")
    }

    @Test("A foreign change to another field of the same item skips nothing either")
    func anUnrelatedFieldOfTheSameItemApplies() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        store.transient.beginRename(of: card1, currentTitle: "First")
        store.transient.updateRenameDraft("Renamed")
        store.commitRename()

        try Foreign.restyle(fixture, card1Path, background: "red")
        history.undo()

        let undone = try document(fixture, card1Path)
        #expect(undone.title.value == "First", "the rename walked back")
        #expect(undone.background.value == "red", "their colour survived it")
        #expect(store.banners.signposts.isEmpty)
    }

    @Test("A foreign rank change skips a reorder")
    func aForeignRankSkipsAReorder() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        store.moveLane(lane1, toIndex: 1)
        try Foreign.setOrder(fixture, Ident.lane1, to: 9000)

        history.undo()

        #expect(try document(fixture, Ident.lane1).order.value == 9000)
        #expect(store.banners.signposts.map(\.message) == ["Undo skipped — 'Todo' changed outside Lanework"])
    }

    @Test("A foreign body edit skips the Edit session's step — body steps compare bytes")
    func aForeignBodyEditSkipsTheSessionStep() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)
        let original = try FrontmatterDocument.parse(fixture.indexText(card1Path)).body

        let session = CardBodyEditSession()
        session.save = { [weak store] text in store?.writeCardBody(inCard: card1, body: text) ?? .vanished }
        session.registerUndo = { [weak store] prior, new in
            store?.registerBodyEdit(inCard: card1, priorBody: prior, newBody: new)
        }
        session.adopt(diskBody: original)
        session.edited("Mine.\n")
        _ = session.flush()
        session.endEditSession()
        #expect(history.undoActionName == "Edit Card")

        // One character of difference is a different body: the step wrote every byte of the span.
        try Foreign.setBody(fixture, card1Path, to: "Mine.\nAnd theirs.\n")
        history.undo()

        #expect(try FrontmatterDocument.parse(fixture.indexText(card1Path)).body == "Mine.\nAnd theirs.\n")
        #expect(store.banners.signposts.map(\.message) == ["Undo skipped — 'First' changed outside Lanework"])
    }

    // MARK: The banner

    @Test("A batch names the step, since there is no single item to name")
    func aBatchStepNamesItself() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        store.applyStyle(to: .items([card1, card2, card3]), background: .set("red"))
        try Foreign.restyle(fixture, card2Path, background: "green")

        history.undo()

        #expect(store.banners.signposts.map(\.message)
            == ["Undo skipped — 'Restyle 3 Cards' changed outside Lanework"])
        #expect(try document(fixture, card1Path).background.value == "red", "all or nothing: no half-applied batch")
    }

    @Test("The skip row is an info-tone signpost — dismissable, and never an error")
    func theSkipRowIsASignpost() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        store.delete([card1])
        try Foreign.purge(fixture, ".trash/\(Ident.card1)")
        history.undo()

        let row = try #require(store.bannerRows.last)
        #expect(row.tone == .info)
        #expect(row.dismissID != nil, "one-shot lifecycle: the user clears it, nothing expires it")
        #expect(store.banners.oneShots.isEmpty)
        #expect(store.banners.losses.isEmpty)
    }

    // MARK: Redo

    @Test("Redo validates the same way, and says so in its own verb")
    func redoStalenessIsSymmetric() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        store.transient.beginRename(of: card1, currentTitle: "First")
        store.transient.updateRenameDraft("Renamed")
        store.commitRename()
        history.undo()
        #expect(try document(fixture, card1Path).title.value == "First")

        // Somebody writes over the state the undo left, so the *forward* write is now the stale one.
        try Foreign.rename(fixture, card1Path, to: "Theirs")
        history.redo()

        #expect(try document(fixture, card1Path).title.value == "Theirs")
        #expect(store.banners.signposts.map(\.message)
            == ["Redo skipped — 'Renamed' changed outside Lanework"])
        #expect(history.canRedo == false)
    }

    @Test("An undone create redoes only onto the hole it left")
    func redoOfACreateChecksTheHole() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)
        let before = try fixture.entryNames("")

        store.createLane()
        let created = try #require(try fixture.entryNames("").first { !before.contains($0) })
        history.undo()
        #expect(fixture.exists(created) == false)

        // Somebody puts a folder back at that identity — the redo's `recreateItem` refuses to
        // clobber, so validation is what turns that into a skip rather than a write failure.
        try fixture.item(created, Item.rich(order: "4096", title: "Theirs"))
        history.redo()

        #expect(try document(fixture, created).title.value == "Theirs")
        #expect(store.banners.signposts.count == 1)
        #expect(store.banners.oneShots.isEmpty, "skipped before the Writer was ever reached")
    }
}

// MARK: - Stale versus failed

/// The distinction 13 leaves to the implementation and this milestone settles: a **stale** step is
/// one the board has moved past — dropped, explained by the info row, ⌘Z falls through. A **failed**
/// one is a step the user still means to cross, refused by a condition that is usually momentary —
/// so it stays put, the ordinary write-failure error row says why, and ⌘Z retries it.
@MainActor
@Suite("Undo ▸ stale versus failed")
struct FailedCrossingTests {

    @Test("An inverse that cannot be written keeps its step, and banners as a write failure")
    func aFailedInverseKeepsItsStep() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        store.transient.beginRename(of: card1, currentTitle: "First")
        store.transient.updateRenameDraft("Renamed")
        store.commitRename()

        // The read and the parse succeed — so validation passes, and the step is genuinely current —
        // and then the atomic replace has nowhere to land its temp file.
        try FileManager.default.setAttributes([.posixPermissions: 0o500], ofItemAtPath: fixture.url(card1Path).path)
        history.undo()

        #expect(try document(fixture, card1Path).title.value == "Renamed", "nothing landed")
        #expect(store.banners.oneShots.count == 1, "the ordinary write-failure row, not a skip")
        #expect(store.banners.signposts.isEmpty)
        #expect(history.canUndo, "the step stays: a full disk is not a reason to lose the way back")
        #expect(history.undoActionName == "Rename Card")
        #expect(history.canRedo == false)

        // And when the condition clears, the same ⌘Z works.
        try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: fixture.url(card1Path).path)
        history.undo()

        #expect(try document(fixture, card1Path).title.value == "First")
        #expect(history.canRedo)
    }

    @Test("A failed crossing stops rather than falling through to the steps below it")
    func aFailedCrossingDoesNotFallThrough() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        store.delete([card2])
        store.transient.beginRename(of: card1, currentTitle: "First")
        store.transient.updateRenameDraft("Renamed")
        store.commitRename()

        try FileManager.default.setAttributes([.posixPermissions: 0o500], ofItemAtPath: fixture.url(card1Path).path)
        history.undo()

        #expect(fixture.exists(".trash/\(Ident.card2)"),
                "the step below was never reached — a refused disk is not a reason to attempt more")
        #expect(history.undoActionName == "Rename Card")
    }
}

// MARK: - The read-only lock

/// "Every read-only lock ... disables Undo/Redo with the other mutating commands; the stack itself
/// survives the lock and resumes when it clears" (13-native-undo.md ▸ Rules).
@MainActor
@Suite("Undo ▸ the read-only lock")
struct LockedBoardUndoTests {

    @Test("A locked board disables Undo and Redo — and the stack is still there when it clears")
    func theLockDisablesAndTheStackSurvives() async throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)
        let manager = BoardUndoManager(history: history, isReadOnly: { [weak store] in store?.isReadOnly ?? false })

        store.transient.beginRename(of: card1, currentTitle: "First")
        store.transient.updateRenameDraft("Renamed")
        store.commitRename()
        #expect(manager.canUndo)

        store.enterVanishedRootLock()

        #expect(manager.canUndo == false, "disabled with every other mutating command")
        #expect(manager.canRedo == false)
        #expect(history.canUndo, "the stack itself survives the lock")
        #expect(manager.undoMenuItemTitle == "Undo Rename Card", "a disabled row keeps its name")

        // The lock clears on the next successful reload, and the same ⌘Z crosses the same step.
        await reload(store)
        #expect(store.isReadOnly == false)
        #expect(manager.canUndo)

        manager.undo()
        #expect(try document(fixture, card1Path).title.value == "First")
    }

    @Test("A crossing that starts anyway is refused without losing the step")
    func aCrossingUnderTheLockLosesNothing() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        store.transient.beginRename(of: card1, currentTitle: "First")
        store.transient.updateRenameDraft("Renamed")
        store.commitRename()
        store.enterVanishedRootLock()

        history.undo()

        #expect(try document(fixture, card1Path).title.value == "Renamed", "the lock refused the write")
        #expect(history.canUndo, "a refusal is a failure, not a staleness — the step stays")
        #expect(store.banners.signposts.isEmpty, "the standing lock row is the message")
        #expect(store.banners.oneShots.isEmpty, "and a refused write posts nothing of its own")
    }
}

// MARK: - The phrase vocabulary

@Suite("Undo ▸ the phrase vocabulary")
struct HistoryPhraseTests {

    @Test("Singular and plural follow 06's folding")
    func pluralFolding() {
        #expect(HistoryPhrase.name(.move, kind: .card) == "Move Card")
        #expect(HistoryPhrase.name(.move, kind: .card, count: 3) == "Move 3 Cards")
        #expect(HistoryPhrase.name(.rename, kind: .lane) == "Rename Lane")
        #expect(HistoryPhrase.name(.resize, kind: .lane, count: 2) == "Resize 2 Lanes")
        #expect(HistoryPhrase.name(.delete, kind: .card, count: 12) == "Delete 12 Cards")
    }

    @Test("A count of one or less is the singular, and the board is always singular")
    func degenerateCounts() {
        #expect(HistoryPhrase.name(.add, kind: .card, count: 1) == "Add Card")
        #expect(HistoryPhrase.name(.add, kind: .card, count: 0) == "Add Card")
        #expect(HistoryPhrase.name(.restyle, kind: .board, count: 4) == "Restyle Board")
    }

    @Test("The phrase never spells the verb the platform composes")
    func noUndoPrefix() {
        for verb in HistoryPhrase.Verb.allCases {
            let phrase = HistoryPhrase.name(verb, kind: .card)
            #expect(phrase.hasPrefix("Undo") == false)
            #expect(phrase.hasPrefix("Redo") == false)
        }
    }
}

// MARK: - The trash's two doors

/// 13-native-undo.md ▸ Interaction with the trash: "⌫'s undo is the move back ... a restore-by-move
/// undoes the same way in reverse. **The stack and the trash never conflict** — they are the same
/// folder moves addressed by recency instead of by selection."
///
/// The per-operation round trips live in `TrashUndoTests` above. What is here is the *interplay*:
/// that the two doors reach one state rather than two similar ones, and that alternating between
/// them leaves a stack that crosses cleanly in both directions.
@MainActor
@Suite("Undo ▸ the trash and the stack are the same folder moves")
struct TrashInterplayTests {

    /// 13-native-undo.md ▸ Interaction with the trash: "The stack and the trash never conflict — they
    /// are the same folder moves addressed by recency instead of by selection."
    @Test("Undoing a delete lands exactly where a manual move-out would — the same bytes")
    func theTwoDoorsReachOneState() async throws {
        // Two identical boards, one per door: the claim is about a *state*, so the honest comparison
        // is the whole board read off disk, not the one field each path happens to write.
        let byUndo = try makeBoard()
        defer { byUndo.tearDown() }
        let byMove = try makeBoard()
        defer { byMove.tearDown() }
        let origin = try boardTexts(byUndo)

        let (undoStore, history) = try makeStore(byUndo)
        undoStore.delete([card1])
        history.undo()

        let (moveStore, _) = try makeStore(byMove)
        moveStore.delete([card1])
        // The move-out reads the trash side of the snapshot, so it has to see the card arrive there
        // first — which is the one-way flow, not a test artefact.
        await reload(moveStore)
        moveStore.moveCards([card1], toLane: lane1, at: 0)

        expectSameBoard(try boardTexts(byUndo), try boardTexts(byMove), "the two doors")
        expectSameBoard(try boardTexts(byUndo), origin, "undo against the board it started from")
        #expect(byUndo.exists(card1Path))
        #expect(byMove.exists(card1Path))
    }

    @Test("Undoing a delete is position-preserving — the card comes back where it was, not at an end")
    func undoRestoresInPlace() async throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)

        /// The lane's cards in display order, read through the loader — the order the board actually
        /// renders, rather than the ranks it is derived from.
        func laneCards() throws -> [String] {
            let result = try BoardLoader.load(boardRoot: fixture.root)
            let lane = try #require(result.model.lanes.first { $0.id == lane1 })
            return lane.cards.map(\.id.rawValue)
        }
        let before = try laneCards()
        #expect(before == [Ident.card1, Ident.card2, Ident.card3], "the middle card is genuinely in the middle")

        // The middle of three: an implementation that restored by appending would pass on a first or
        // last card and fail here.
        store.delete([card2])
        await reload(store)
        #expect(try laneCards() == [Ident.card1, Ident.card3])

        history.undo()

        #expect(try laneCards() == before, "13: the undo returns the card to its source lane and rank")
        #expect(try document(fixture, card2Path).order.value == 2048, "the rank it held all along")
    }

    @Test("Alternating the two doors leaves a stack that crosses cleanly, both ways")
    func interleavedDoorsCrossBothWays() async throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)
        let live = try boardTexts(fixture)

        // Three gestures over one card, alternating the doors: ⌫, move back out, ⌫.
        store.delete([card1])
        await reload(store)
        #expect(fixture.exists(".trash/\(Ident.card1)"))

        store.moveCards([card1], toLane: lane1, at: 0)
        await reload(store)
        #expect(fixture.exists(card1Path))

        store.delete([card1])
        await reload(store)
        #expect(history.undoActionName == "Delete Card")
        let trashedBoard = try boardTexts(fixture)

        // Back up the stack: delete → move → delete, each crossed in reverse.
        history.undo()
        #expect(fixture.exists(card1Path))
        #expect(history.undoActionName == "Move Card")

        history.undo()
        #expect(fixture.exists(".trash/\(Ident.card1)"), "back in the trash the move took it out of")
        #expect(history.undoActionName == "Delete Card")

        history.undo()
        #expect(history.canUndo == false)
        expectSameBoard(try boardTexts(fixture), live, "three doors back")

        // And forward again, the same three steps in the other direction.
        history.redo()
        #expect(fixture.exists(".trash/\(Ident.card1)"))
        history.redo()
        #expect(fixture.exists(card1Path))
        history.redo()
        #expect(history.canRedo == false)
        expectSameBoard(try boardTexts(fixture), trashedBoard, "three doors forward")
        #expect(store.banners.signposts.isEmpty, "nothing was stale: the doors never collided")
        #expect(store.banners.oneShots.isEmpty)
    }
}

// MARK: - The capstone

/// **A whole session, unwound and wound back up.** 13-native-undo.md's promise, taken literally: a
/// user creates, moves, styles, renames and deletes without touching the mouse, then holds ⌘Z until
/// the board is the one they opened — and ⇧⌘Z back to the one they built.
///
/// The assertion is the *board*, read off disk in full: every `index.md`, byte for byte but for the
/// `modified` stamps every app write owns (`expectSameBoard`, whose note records why that is the
/// right precision — an inverse is an ordinary write, and 13 promises state equivalence rather than
/// mtime equality).
///
/// Five gestures rather than one of each kind on purpose: they all name the **same card**, so each
/// step's staleness predicate is validated against a board four other steps have since written to.
/// A chain that only worked on untouched targets would pass a suite of five isolated round trips and
/// fail here.
@MainActor
@Suite("Undo ▸ the full session")
struct FullSessionUndoTests {

    @Test("Create, move, style, rename, delete — then ⌘Z back to the origin and ⇧⌘Z forward again")
    func theWholeSessionRoundTrips() async throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let (store, history) = try makeStore(fixture)
        let origin = try boardTexts(fixture)

        // 1 — create. The one gesture whose undo removes a folder rather than rewriting one.
        store.transient.beginPlaceholder(inLane: lane1, after: card1)
        store.transient.updateDraft("Fresh")
        let created = try #require(store.commitPlaceholder())
        await reload(store)
        #expect(history.undoActionName == "Add Card")

        // 2 — move it to the other lane.
        store.moveCards([created], toLane: lane2, at: 0)
        await reload(store)
        #expect(history.undoActionName == "Move Card")

        // 3 — style it.
        store.applyStyle(to: .items([created]), background: .set("red"), icon: .set("flag"))
        await reload(store)
        #expect(history.undoActionName == "Restyle Card")

        // 4 — rename it.
        store.transient.beginRename(of: created, currentTitle: "Fresh")
        store.transient.updateRenameDraft("Renamed")
        store.commitRename()
        await reload(store)
        #expect(history.undoActionName == "Rename Card")

        // 5 — delete it.
        store.delete([created])
        await reload(store)
        #expect(history.undoActionName == "Delete Card")

        let built = try boardTexts(fixture)
        #expect(built.count == origin.count + 1, "one new card, and nothing else arrived")

        // ⌘Z ×5. The Edit menu's row renames itself on every press — the platform composes
        // "Undo <phrase>" from exactly these names (`BoardUndoManager`).
        for next in ["Rename Card", "Restyle Card", "Move Card", "Add Card"] {
            history.undo()
            #expect(history.undoActionName == next)
        }
        history.undo()

        #expect(history.canUndo == false, "five gestures, five steps, no more and no fewer")
        #expect(history.redoActionName == "Add Card")
        expectSameBoard(try boardTexts(fixture), origin, "unwound")
        #expect(store.banners.signposts.isEmpty, "nothing went stale under its own session")
        #expect(store.banners.oneShots.isEmpty, "and nothing failed to write")

        // ⇧⌘Z ×5, back to the board the session built.
        for next in ["Move Card", "Restyle Card", "Rename Card", "Delete Card"] {
            history.redo()
            #expect(history.redoActionName == next)
        }
        history.redo()

        #expect(history.canRedo == false)
        #expect(history.undoActionName == "Delete Card")
        expectSameBoard(try boardTexts(fixture), built, "rewound")
        #expect(store.banners.signposts.isEmpty)
        #expect(store.banners.oneShots.isEmpty)
    }
}
