import AppKit
import Foundation
import SwiftUI
import Testing
@testable import Kanban

/// **The board strip's render cost, measured on a real hosted board** (`BoardRenderMetrics`,
/// RENDER-INSTRUMENTATION.md).
///
/// Ported from the pathfinder's `BoardRenderPerformanceTests`, and the same instrument: a whole
/// `BoardView` in an off-screen `NSWindow`, driven exactly the way `FolderWatcher` drives it — one
/// `handleWatcherEvent(.treeChanged(.foreign))` per step — with `BoardRenderMetrics` read around
/// each. What is asserted is not a timing (a timing is a fact about this machine) but the two
/// **invariants the 2026-07-31 drag-performance work is a claim about**:
///
///  1. **A reload that landed value-equal re-runs zero bodies.** Since 988a724 an equal walk skips
///     the snapshot assignment entirely (`landedReloads` moves, `snapshotGeneration` does not), so
///     this holds by construction rather than by a gate — and pinning it here is what makes the
///     *construction* checkable: a future edit that assigned an equal model back would break this
///     test rather than quietly costing a whole-board render pass on every `.git` touch.
///  2. **A one-card edit re-renders a handful of bodies, not the board.** This is the equatable
///     gates' invariant (84f909a; `ViewEquatableTests` pins the comparison list, this pins the
///     effect), and the one that a gate silently coming undone would break.
///
/// ...and the counter-invariant that keeps the gates honest: **selecting a card must still repaint
/// it**. A gate that suppressed that would be a broken board, not a fast one.
///
/// ### What the first run found (2026-08-01)
///
/// Invariant 1 holds exactly. Invariant 2 holds for **cards** and fails for **containers**: a
/// one-card edit costs 1 card body out of 180, and one lane body per lane on the board — 6 of 6, and
/// 12 of 12 when the board is doubled. The cause is not the gate coming undone; it is
/// `LaneView.headerInk` reading `store.snapshot.background`, which under Observation's
/// whole-property tracking subscribes every lane body to the entire snapshot.
/// `theLaneCostFollowsTheBoard` pins both halves — the comparison is right, and it is never asked —
/// and the container budget in `aOneCardEditIsNotAWholeBoardRebuild` is written to the number the
/// tree actually produces, with the reason, rather than to the pathfinder's 4.
///
/// ### What is hosted
///
/// The **whole `BoardView`**, in a 1600×1000 off-screen window, with a real `AppModel` in the
/// environment (its registry file and clipboard staging redirected into the test's own temp folder,
/// `AppModelTests`' idiom). Not a lane-strip stand-in: the gates exist because `BoardView`'s body
/// reads the drag session and therefore re-runs for reasons that have nothing to do with any one
/// lane, and a harness that hosted `LaneView` directly would be asserting about a parent that does
/// not exist. The scaffolding turned out to be four values — the window closure, a
/// `TrashConfirmations`, an `openCard` closure and a `BoardSearchPresentation` — all of which a test
/// can supply honestly.
///
/// Boards are real loads off real temp trees, `ViewEquatableTests`' reason.

// MARK: - Fixture

/// The board's shape. Wide enough that "the whole board rebuilt" and "one card repainted" are
/// unmistakably different numbers, small enough that hosting it stays a unit test.
private let laneCount = 6
private let cardsPerLane = 30

/// Ids are derived rather than drawn from `Ident`, because this fixture needs 180 of them.
private func laneName(_ lane: Int) -> String {
    String(format: "1%07d-1111-4111-8111-111111111111", lane)
}

private func cardName(_ lane: Int, _ card: Int) -> String {
    String(format: "2%03d%04d-2222-4222-8222-222222222222", lane, card)
}

@MainActor
private func makeFixture(lanes laneCount: Int = laneCount, cards cardsPerLane: Int = cardsPerLane)
    throws -> WriterFixture {
    let fixture = try WriterFixture()
    try fixture.board(title: "Perf Board")
    for lane in 0..<laneCount {
        try fixture.lane(laneName(lane), order: "\((lane + 1) * 1024)", title: "Lane \(lane)")
        for card in 0..<cardsPerLane {
            try fixture.card(
                cardName(lane, card),
                in: laneName(lane),
                order: "\((card + 1) * 1024)",
                title: "Card \(lane)-\(card)",
                body: "Body text for card \(lane)-\(card)."
            )
        }
    }
    return fixture
}

// MARK: - Hosting

/// `BoardWindowHost`'s one relevant job, reproduced: read the app-wide zoom level and inject it as
/// the strip's ruler (03-board-ui.md ▸ Layout — zoom).
///
/// A wrapper rather than a `.environment(\.boardZoom, …)` on the hosted root, because the modifier's
/// argument is evaluated once when the root value is built and an `NSHostingView`'s root is a stored
/// value. Reading `appModel.zoom` inside a `body` is what makes a level change re-run this view and
/// therefore re-publish the environment — which is exactly the propagation path the real window uses,
/// and exactly what the zoom invariant below is a claim about.
private struct ZoomedBoard: View {

    let store: BoardStore
    let window: @MainActor () -> NSWindow?
    let confirmations: TrashConfirmations
    let openCard: @MainActor (ItemID) -> Void
    let search: BoardSearchPresentation

    @Environment(AppModel.self) private var appModel

    var body: some View {
        BoardView(
            store: store,
            window: window,
            confirmations: confirmations,
            openCard: openCard,
            search: search
        )
        .environment(\.boardZoom, appModel.zoom.context)
    }
}

/// Everything the hosted board needs to stay alive for the length of a test — an `NSHostingView`
/// whose window is released the moment nothing holds it would stop rendering mid-measurement.
@MainActor
private final class HostedBoard {
    let store: BoardStore
    let appModel: AppModel
    let window: NSWindow
    let view: NSView
    private let scratch: URL

    /// The preferences domain the model's app-wide state persists into — redirected for
    /// `registryStorageURL`'s reason. The zoom level lives here, and a suite that used `.standard`
    /// would leave the developer's own boards zoomed.
    private let preferencesDomain: String

    init(store: BoardStore, scratch: URL) {
        self.store = store
        self.scratch = scratch
        preferencesDomain = "dev.rzen.indie.Kanban.render-perf.\(UUID().uuidString)"
        appModel = AppModel(
            registryStorageURL: scratch.appendingPathComponent("board-registry.json"),
            clipboardStagingRoot: scratch.appendingPathComponent("Clipboard", isDirectory: true),
            preferences: UserDefaults(suiteName: preferencesDomain)!
        )
        let window = NSWindow(
            contentRect: NSRect(x: 0, y: 0, width: 1600, height: 1000),
            styleMask: [.titled], backing: .buffered, defer: false
        )
        self.window = window
        let root = ZoomedBoard(
            store: store,
            window: { [weak window] in window },
            confirmations: TrashConfirmations(),
            openCard: { _ in },
            search: BoardSearchPresentation()
        )
        .environment(appModel)
        let hosting = NSHostingView(rootView: root)
        hosting.frame = NSRect(x: 0, y: 0, width: 1600, height: 1000)
        view = hosting
        window.contentView = hosting
        // `orderBack` rather than `makeKeyAndOrderFront`: the board must lay out and render, and it
        // must not steal focus from whatever is running the suite.
        window.orderBack(nil)
        settle()
    }

    deinit {
        window.orderOut(nil)
        window.contentView = nil
        try? FileManager.default.removeItem(at: scratch)
        UserDefaults.standard.removePersistentDomain(forName: preferencesDomain)
    }

    /// Pumps the main run loop until SwiftUI has flushed its pending updates and laid the tree out.
    ///
    /// Several short spins rather than one long one, the pathfinder's harness note: an update
    /// scheduled *by* the previous flush needs another turn before it runs. `layoutSubtreeIfNeeded`
    /// and `displayIfNeeded` are what actually force the bodies — a hosting view with no display
    /// pass pending evaluates nothing, and the measurement would read zero for the wrong reason.
    func settle(turns: Int = 6) {
        for _ in 0..<turns {
            RunLoop.main.run(until: Date().addingTimeInterval(0.02))
            view.layoutSubtreeIfNeeded()
            window.displayIfNeeded()
        }
    }

    /// One reload through the store's one inbound door, then a settle — exactly what `FolderWatcher`
    /// causes when an agent or an editor writes into the tree.
    func reload() async {
        store.handleWatcherEvent(.treeChanged(.foreign))
        await store.awaitQuiescence()
        settle()
    }
}

@MainActor
private func host(_ fixture: WriterFixture) throws -> HostedBoard {
    let scratch = FileManager.default.temporaryDirectory
        .appendingPathComponent("BoardRenderPerf-\(UUID().uuidString)", isDirectory: true)
    try FileManager.default.createDirectory(at: scratch, withIntermediateDirectories: true)
    return HostedBoard(store: try BoardStore(rootURL: fixture.root), scratch: scratch)
}

/// The counters, snapshotted — reading them into a value keeps a `#expect` from racing a later body.
private struct Cost {
    var strips: Int
    var cards: Int
    var containers: Int
    var measures: Int
    var cacheHits: Int
    var sizeThatFits: Int
    var places: Int

    @MainActor
    init() {
        strips = BoardRenderMetrics.stripBodyEvaluations
        cards = BoardRenderMetrics.cardBodyEvaluations
        containers = BoardRenderMetrics.containerBodyEvaluations
        measures = BoardRenderMetrics.masonryMeasurements
        cacheHits = BoardRenderMetrics.masonryCacheHits
        sizeThatFits = BoardRenderMetrics.masonrySizeThatFitsCalls
        places = BoardRenderMetrics.masonryPlaceCalls
    }

    var summary: String {
        "\(strips) strip bodies, \(containers) container bodies, \(cards) card bodies, "
            + "\(measures) measures (+\(cacheHits) cached) over "
            + "\(sizeThatFits) sizeThatFits / \(places) place"
    }
}

// MARK: - The invariants

@MainActor
@Suite("The board strip's render cost", .serialized)
struct BoardRenderPerformanceTests {

    @Test("The counters see a real hosted board — the harness itself, before anything is asserted on it")
    func theHarnessActuallyRenders() throws {
        let fixture = try makeFixture()
        defer { fixture.tearDown() }

        BoardRenderMetrics.reset()
        let board = try host(fixture)
        let first = Cost()
        withExtendedLifetime(board) {}

        // A zero here would make every other assertion in this file vacuous — the invariants below
        // are all "≤", and a harness that rendered nothing satisfies them perfectly.
        #expect(first.cards >= laneCount * cardsPerLane,
                "the first paint drew \(first.cards) of \(laneCount * cardsPerLane) card faces")
        #expect(first.containers >= laneCount)
        #expect(first.measures > 0, "the masonry measured nothing")
        print("── first paint — \(first.summary)")
    }

    @Test("A reload that landed value-equal re-runs zero bodies")
    func aValueEqualReloadCostsNothing() async throws {
        let fixture = try makeFixture()
        defer { fixture.tearDown() }
        let board = try host(fixture)

        let generationBefore = board.store.snapshotGeneration
        let landedBefore = board.store.landedReloads

        BoardRenderMetrics.reset()
        await board.reload()
        let idle = Cost()

        print("── reload, nothing changed — \(idle.summary)")

        // The walk happened and found the board it already had (988a724).
        #expect(board.store.landedReloads == landedBefore + 1, "the reload did not land")
        #expect(board.store.snapshotGeneration == generationBefore,
                "a value-equal reload moved the snapshot generation")

        #expect(idle.cards == 0, "an unchanged reload re-rendered \(idle.cards) card bodies")
        #expect(idle.containers == 0, "an unchanged reload re-rendered \(idle.containers) container bodies")
    }

    @Test("A one-card edit re-renders a handful of bodies, not the board")
    func aOneCardEditIsNotAWholeBoardRebuild() async throws {
        let fixture = try makeFixture()
        defer { fixture.tearDown() }
        let board = try host(fixture)
        let total = laneCount * cardsPerLane

        // A foreign edit to exactly one card, the way an external editor makes one per keystroke.
        try fixture.card(
            cardName(3, 17), in: laneName(3), order: "\(18 * 1024)",
            title: "Card 3-17 edited", body: "Body text for card 3-17, now edited."
        )

        BoardRenderMetrics.reset()
        await board.reload()
        let edit = Cost()

        print("── reload, ONE card edited — \(edit.summary)")

        #expect(board.store.snapshotGeneration > 0, "the edit never landed")

        // **The card gate's invariant, at the pathfinder's budget exactly.** Loose enough to absorb
        // SwiftUI evaluating a body more than once per update (the first paint runs each face three
        // times), and two orders of magnitude under the \(total) an un-gated tree costs — which is
        // what this measured before `CardFaceView.==`.
        #expect(edit.cards <= 8, "a one-card external edit re-rendered \(edit.cards) of \(total) card faces")

        // **The container budget is `laneCount`, not the pathfinder's 4 — and that is a diagnosed
        // finding, not a slack threshold.** Every lane's body re-runs on every model-changing reload,
        // and `LaneView.==` never gets a say, because `LaneView.body` reads `store.snapshot`:
        //
        //     LaneView.body → header → .boardTextInk(headerInk) → headerInk
        //         → BoardTextInk.scheme(forBoardBackground: store.snapshot.background, …)
        //
        // Observation tracks whole *properties*, so reading `.background` off `store.snapshot`
        // subscribes that body to the entire snapshot. A reload that changes one card anywhere
        // assigns `store.snapshot` and invalidates every lane on the board **directly** — and a
        // direct invalidation is precisely what `.equatable()` has no say over (`LaneView.==`'s own
        // doc comment says so; the gate still does its job on every *parent-driven* pass, which is
        // what `aValueEqualReloadCostsNothing` measures at 0 containers for a strip pass that did
        // happen).
        //
        // `theLaneCostFollowsTheBoard` below pins both halves of that diagnosis. Fixing it means
        // resolving the board's ink once in `BoardView` and passing it down as a compared parameter,
        // the way `slotWidth` and `columns` already are — out of scope for an instrumentation card,
        // and the budget here is written to the number the tree actually produces so the *card* gate
        // stays assertable in the meantime.
        #expect(edit.containers <= laneCount,
                "a one-card external edit re-rendered \(edit.containers) bodies for \(laneCount) containers")
    }

    /// **The comments chip's render-cost guard** (design ruling 2026-08-09, card e729e30a): a
    /// comment landing on one card must cost this suite's card budget, not the board's — proof that
    /// `Card.commentCount` riding inside the already-compared `card` parameter (no new
    /// `CardFaceView` parameter, no new Observable read) actually holds under a real hosted board,
    /// not just in the equality-gate unit tests (`ViewEquatableTests.aCommentCountChangeIsADifference`
    /// pins the gate; this pins what the gate is *for*). Structurally `aOneCardEditIsNotAWholeBoardRebuild`,
    /// a comment folder in place of an edited `index.md`.
    @Test("A comment added to one card re-renders a handful of bodies, not the board")
    func aOneCommentAddedIsNotAWholeBoardRebuild() async throws {
        let fixture = try makeFixture()
        defer { fixture.tearDown() }
        let board = try host(fixture)
        let total = laneCount * cardsPerLane

        // A foreign comment arriving under exactly one card — the shape a card window's post, or an
        // agent's own `mkdir` + `index.md`, leaves on disk.
        try fixture.item(
            "\(laneName(3))/\(cardName(3, 17))/comments/\(UUID().uuidString.lowercased())",
            "---\nschema: 1\n---\nA comment.\n"
        )

        BoardRenderMetrics.reset()
        await board.reload()
        let commented = Cost()

        print("── reload, ONE comment added — \(commented.summary)")

        #expect(board.store.snapshotGeneration > 0, "the comment never landed")
        #expect(commented.cards <= 8,
                "one comment on one card re-rendered \(commented.cards) of \(total) card faces")
        #expect(commented.containers <= laneCount,
                "one comment on one card re-rendered \(commented.containers) bodies for \(laneCount) containers")
    }

    @Test("Every lane re-runs on a one-card edit, and not because its gate compared unequal")
    func theLaneCostFollowsTheBoard() async throws {
        let wide = laneCount * 2
        let fixture = try makeFixture(lanes: wide, cards: 15)
        defer { fixture.tearDown() }
        let board = try host(fixture)

        // An untouched sibling's value, either side of an edit to a different lane.
        let siblingBefore = try #require(board.store.snapshot.lanes.first { $0.id == ItemID(rawValue: laneName(5)) })
        try fixture.card(
            cardName(3, 7), in: laneName(3), order: "\(8 * 1024)",
            title: "Card 3-7 edited", body: "Body text for card 3-7, now edited."
        )

        BoardRenderMetrics.reset()
        await board.reload()
        let edit = Cost()
        let siblingAfter = try #require(board.store.snapshot.lanes.first { $0.id == ItemID(rawValue: laneName(5)) })

        print("── reload, ONE card edited, \(wide) lanes — \(edit.summary)")

        // **Half one of the diagnosis: the gate's comparison is right.** An untouched sibling lane is
        // the same `Lane` value across the reload, and every other member `LaneView.==` compares —
        // `columns`, `slotWidth`, the store, the band, the drop context — is a window-lived constant
        // here. So the gate would suppress, if it were ever asked.
        #expect(siblingBefore == siblingAfter, "an untouched lane came back from the reload unequal")

        // **Half two: it is never asked.** Doubling the lane count doubles the container cost, which
        // is the signature of "every lane once" rather than "the edited lane several times". The
        // cause is `LaneView.headerInk`'s `store.snapshot` read — see
        // `aOneCardEditIsNotAWholeBoardRebuild` for the chain.
        //
        // Deliberately `>=`, as a tripwire in *both* directions: this failing because the number went
        // **down** means the snapshot read has been hoisted out of `LaneView` and the container
        // budget above should come down to the pathfinder's 4.
        #expect(edit.containers >= wide,
                "a one-card edit cost \(edit.containers) container bodies on a \(wide)-lane board — if that is below \(wide), the headerInk finding is fixed")

        // The card gate is unaffected by any of it, on a board twice as wide.
        #expect(edit.cards <= 8, "a one-card edit re-rendered \(edit.cards) of \(wide * 15) card faces")
    }

    /// **The `labels` submenu's load-bearing render claim** (2026-08-09, Pipeline card 28c79ffe;
    /// `CardFaceView.labelsMenu`).
    ///
    /// The submenu names up to twelve labels ranked across the **whole board**, and `.contextMenu`'s
    /// content closure is not lazy — SwiftUI builds every row of every face's menu on every ordinary
    /// body pass. Deriving that ranking per face would be a board walk per card per pass, the exact
    /// shape this suite exists to rule out. So the walk is cached on the store (`BoardStore.labelIndex`,
    /// re-derived once per applied snapshot) and its assignment is **equality-gated**.
    ///
    /// That gate is what this test is about. Ungated, every reload would assign the property, every
    /// face that reads it would invalidate *directly* — past `CardFaceView.==` entirely, the way the
    /// old `store.selection` read did — and a one-card edit would cost the whole board again. This
    /// asserts the number `aOneCardEditIsNotAWholeBoardRebuild` asserts, on a board where every card
    /// carries labels, so a missing gate shows up here as \(laneCount * cardsPerLane) instead of a
    /// handful.
    ///
    /// A label change that genuinely moves the board's vocabulary is a different matter and is not
    /// pinned here: the universe really did change, every menu really is different, and a full repaint
    /// on a deliberate one-per-gesture event is the class `zoomRepaintsTheCardFaces` already blesses.
    @Test("An edit to a labelled card costs a handful of faces — the label universe's gate holds")
    func labelsDoNotMakeEveryEditABoardRebuild() async throws {
        let fixture = try makeFixture()
        defer { fixture.tearDown() }
        // Every card labelled, so the universe is real and every face's menu has twelve rows to build.
        for lane in 0..<laneCount {
            for card in 0..<cardsPerLane {
                try fixture.item("\(laneName(lane))/\(cardName(lane, card))", """
                ---
                schema: 1
                kind: card
                title: Card \(lane)-\(card)
                order: \((card + 1) * 1024)
                labels: ["bug", "ui", "lane\(lane)"]
                ---
                Body text for card \(lane)-\(card).

                """)
            }
        }
        let board = try host(fixture)
        let total = laneCount * cardsPerLane
        #expect(board.store.labelIndex.names.count == laneCount + 2, "the universe is real")

        // One card's *body* edited — its labels untouched, so the universe does not move.
        try fixture.item("\(laneName(3))/\(cardName(3, 17))", """
        ---
        schema: 1
        kind: card
        title: Card 3-17
        order: \(18 * 1024)
        labels: ["bug", "ui", "lane3"]
        ---
        Body text for card 3-17, now edited.

        """)

        BoardRenderMetrics.reset()
        await board.reload()
        let edit = Cost()

        print("── reload, ONE labelled card edited — \(edit.summary)")

        #expect(board.store.snapshotGeneration > 0, "the edit never landed")
        #expect(edit.cards <= 8,
                "a one-card edit on a labelled board re-rendered \(edit.cards) of \(total) card faces")
    }

    @Test("Selecting a card still repaints it — the gate never went too far")
    func selectionStillRepaints() throws {
        let fixture = try makeFixture()
        defer { fixture.tearDown() }
        let board = try host(fixture)

        BoardRenderMetrics.reset()
        board.store.select([ItemID(rawValue: cardName(3, 17))], in: .board)
        board.settle()
        let selected = Cost()

        print("── select one card — \(selected.summary)")

        // Selection changes this face's *styling* (03-board-ui.md § Card face) — something has to
        // run. The other half of the gate: too strict a `==` would show up here as a zero.
        #expect(selected.cards > 0, "selecting a card repainted nothing")

        // **And it costs a handful of faces, not the board.** This used to be "every face on the
        // board" — `CardFaceView.body` read `store.selection` for its own `isSelected`, so under
        // Observation's property-level tracking one click invalidated all \(laneCount * cardsPerLane)
        // of them *directly*, past the gate entirely (RENDER-INSTRUMENTATION.md ▸ Selection is
        // O(board) in card bodies). Selected-ness is a compared parameter now — the lane hoists the
        // selection once and hands each face its answer — so what re-runs is the lane bodies that
        // were subscribed anyway plus the faces whose flag actually flipped.
        //
        // The budget is the one-card-edit budget above and it is loose for the same reason: SwiftUI
        // evaluates a body more than once per update, so a single flipped face is worth several
        // counts. What it rules out is the old shape, which was two orders of magnitude over this.
        #expect(selected.cards <= 8,
                "selecting one card re-rendered \(selected.cards) of \(laneCount * cardsPerLane) card faces")

        #expect(selected.strips >= 1, "the strip did not re-run for a selection change")
    }

    /// **The zoom feature's load-bearing render claim** (03-board-ui.md ▸ Layout — zoom).
    ///
    /// `CardFaceView` is `.equatable()` and its `==` compares nothing that moves with the zoom level:
    /// the card, its role, its store, the marquee and the drop context — all identical across a
    /// ⌘+. The level reaches the faces *only* because it travels in the environment, which
    /// `CardFaceView`'s own note says the gate deliberately does not compare ("SwiftUI invalidates on
    /// those itself").
    ///
    /// That makes this the one assertion standing between the feature and a board that zooms its
    /// lanes while every card face stays 13pt — a failure that would look like a rendering glitch and
    /// actually be an architecture decision quietly coming undone. A zero here is the whole bug.
    @Test("A zoom change repaints the card faces, through the equality gate")
    func zoomRepaintsTheCardFaces() throws {
        let fixture = try makeFixture()
        defer { fixture.tearDown() }
        let board = try host(fixture)

        BoardRenderMetrics.reset()
        board.appModel.zoom.step(.in)
        board.settle()
        let zoomed = Cost()

        print("── zoom in one rung — \(zoomed.summary)")

        #expect(board.appModel.zoom.level != BoardZoom.actualSize, "the level did not actually move")
        #expect(zoomed.cards > 0, "a zoom change repainted no card faces — the equality gate swallowed it")
        #expect(zoomed.containers > 0, "a zoom change repainted no lanes")
        #expect(zoomed.strips >= 1, "the strip did not re-run for a zoom change")
        // The masonry's own cache is keyed on column width, which moves with the card spacing, so a
        // level change must also cost a re-measure rather than replay stale heights.
        #expect(zoomed.measures > 0, "the masonry replayed heights measured on the old ruler")
    }

    /// The counter-invariant: Actual Size costs what it always did. A board nobody zooms must not pay
    /// for the feature existing — the steady state after a reload is still the numbers the two
    /// invariants above pin, with the ruler sitting on the system's own body size.
    @Test("At Actual Size the strip renders on the system's own ruler")
    func actualSizeIsTheUntouchedBoard() throws {
        let fixture = try makeFixture()
        defer { fixture.tearDown() }
        let board = try host(fixture)

        #expect(board.appModel.zoom.isActualSize, "a fresh domain must open unzoomed")
        #expect(board.appModel.zoom.context.bodyPointSize == BoardMetrics.bodyPointSize)

        BoardRenderMetrics.reset()
        board.appModel.zoom.step(.actualSize)
        board.settle()
        let resettled = Cost()

        print("── Actual Size when already there — \(resettled.summary)")

        // `BoardZoomStore.setLevel` refuses an unchanged level outright — `@Observable` notifies on
        // every set, equal or not, so without that guard re-asserting the level the board is already
        // at would re-run the whole strip to draw exactly what it was drawing.
        #expect(resettled.cards == 0, "a no-op Actual Size repainted \(resettled.cards) card faces")
        #expect(resettled.containers == 0, "a no-op Actual Size repainted \(resettled.containers) lanes")
    }
}
