import Foundation
import Testing
@testable import Kanban

/// **The decision surface** (01-storage-format.md § Malformed input, settled 2026-07-31) — the
/// grouping, the vocabulary, the two minted repairs, the skip channel's ride through the session, and
/// the attendance branch that decides whether any of it is reached at all.
///
/// Everything here is asked of values. The surface's rules live in `BoardDecisionSurfaceModel` and
/// its landing rule in a static on the host precisely so a suite with no window can hold the whole
/// decision in its hand: what a class offers, what it defaults to, when Repair and Open may be
/// pressed, and what a repair actually puts on disk.

// MARK: - Fixtures

/// A board whose root has no `schema` and one lane that will not parse — the minimal two-class
/// aggregate, and the shape most of these cases need: one minted repair, one hand-edit.
@MainActor
private func makeTwoClassBoard() throws -> WriterFixture {
    let fixture = try WriterFixture()
    // No `schema` at the root — the this-really-is-a-board gate, defect #1.
    try fixture.item("", "---\ntitle: Two Classes\n---\nA board with two kinds of problem.\n")
    try fixture.item(Ident.lane1, "---\nschema: 1\ntitle: Intact\norder: 1024\n---\n")
    try fixture.item("\(Ident.lane1)/\(Ident.card1)", "---\nschema: 1\ntitle: Card\norder: 1024\n---\n")
    // Unparseable YAML — an unterminated flow sequence, defect #2.
    try fixture.item(Ident.lane2, "---\nschema: 1\ntitle: Broken\norder: 2048\nlabels: [a, b\n---\n")
    return fixture
}

/// A board whose only defects are **below the root** — the skip channel's golden case.
@MainActor
private func makeSkippableBoard() throws -> WriterFixture {
    let fixture = try WriterFixture()
    try fixture.item("", "---\nschema: 1\ntitle: Skippable\n---\n")
    try fixture.item(Ident.lane1, "---\nschema: 1\ntitle: Intact\norder: 1024\n---\n")
    try fixture.item("\(Ident.lane1)/\(Ident.card1)", "---\nschema: 1\ntitle: Card\norder: 1024\n---\n")
    // A card from a newer Lanework: unfixable, so Skip is the only offer.
    try fixture.item("\(Ident.lane1)/\(Ident.card2)", "---\nschema: 2\ntitle: Newer\norder: 2048\n---\n")
    // A lane that will not parse: Open in Editor + Re-check, or Skip.
    try fixture.item(Ident.lane2, "---\nschema: 1\ntitle: Broken\norder: 2048\nlabels: [a, b\n---\n")
    return fixture
}

/// The failure a board refuses with — the input every model here is built from.
@MainActor
private func failure(of fixture: WriterFixture, skipping: Set<String> = []) throws -> BoardLoadFailure {
    do throws(BoardLoadFailure) {
        _ = try BoardLoader.load(boardRoot: fixture.root, skipping: skipping)
        Issue.record("the fixture board loaded — it is supposed to refuse")
        throw BoardLoadFailure(BoardLoadError(path: ".", reason: .notADirectory))
    } catch {
        return error
    }
}

@MainActor
private func makeModel(_ fixture: WriterFixture) throws -> BoardDecisionSurfaceModel {
    BoardDecisionSurfaceModel(failure: try failure(of: fixture), boardRoot: fixture.root)
}

// MARK: - Grouping and defaults

@MainActor
@Suite("Decision surface ▸ grouping and defaults")
struct BoardDecisionSurfaceGroupingTests {

    @Test("Defects group by class, and the classes come in walk order")
    func groupingIsByClassInWalkOrder() throws {
        let fixture = try makeSkippableBoard()
        defer { fixture.tearDown() }
        let model = try makeModel(fixture)

        // The walk is root, then lanes in folder-name order with their cards inside them — so the
        // newer-schema card (under lane 1) is met before the unparseable lane 2.
        #expect(model.sections.map(\.defectClass) == [.newerSchema, .unreadableFrontmatter])
        #expect(model.sections.allSatisfy { $0.rows.count == 1 })
    }

    @Test("One class with several files is one section, in walk order")
    func oneClassIsOneSection() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("", "---\nschema: 1\ntitle: Board\n---\n")
        try fixture.item(Ident.lane1, "---\nschema: 1\ntitle: A\norder: 1024\nlabels: [a\n---\n")
        try fixture.item(Ident.lane2, "---\nschema: 1\ntitle: B\norder: 2048\nlabels: [b\n---\n")

        let model = try makeModel(fixture)

        // "each class section states the defect once, lists the affected files".
        #expect(model.sections.count == 1)
        let section = try #require(model.sections.first)
        #expect(section.defectClass == .unreadableFrontmatter)
        #expect(section.rows.map(\.id) == ["\(Ident.lane1)/index.md", "\(Ident.lane2)/index.md"])
    }

    @Test("Every class preselects its ruled default")
    func classDefaultsAreTheRuledOnes() throws {
        // The minted repairs, at the root where they occur.
        #expect(BoardDefectClass.missingBoardIndex.choices(atPath: "index.md").first == .repair(.mintBoardIndex))
        #expect(BoardDefectClass.missingRootSchema.choices(atPath: "index.md").first == .repair(.stampSchema))
        // "Open in Editor + Re-check, **or** Skip" — the posture leads, the tolerance follows.
        #expect(
            BoardDefectClass.unreadableFrontmatter.choices(atPath: "lane/index.md")
                == [.editAndRecheck, .skip]
        )
        // "an unfixable row … offering only Skip".
        #expect(BoardDefectClass.newerSchema.choices(atPath: "lane/index.md") == [.skip])
    }

    @Test("malformedSchema is seated with the YAML family, root restriction and all")
    func malformedSchemaJoinsTheYAMLFamily() {
        // Redesign Gap bcdd1942: not one of the ruled four, and the same honest posture — the app
        // must not guess what `schema: banana` was meant to be.
        #expect(BoardDefectClass(.malformedSchema(raw: "banana")) == .unreadableFrontmatter)
        #expect(BoardDefectClass(.unparseableYAML(message: "x", line: 2)) == .unreadableFrontmatter)
        // The root restriction it inherits: Editor + Re-check at the root, plus Skip below it.
        #expect(BoardDefectClass.unreadableFrontmatter.choices(atPath: "index.md") == [.editAndRecheck])
    }

    @Test("A root defect is never offered Skip")
    func theRootIsNeverSkippable() throws {
        let fixture = try makeTwoClassBoard()
        defer { fixture.tearDown() }
        let model = try makeModel(fixture)

        let rootRow = try #require(model.rows.first { $0.id == BoardLoader.indexFileName })
        #expect(!rootRow.choices.contains(.skip), "there is no board without its root")
        #expect(rootRow.choice == .repair(.stampSchema))
    }

    @Test("Every row states its path and the walk's own reason")
    func rowsCarryPathAndSpecifics() throws {
        let fixture = try makeTwoClassBoard()
        defer { fixture.tearDown() }
        let model = try makeModel(fixture)

        let broken = try #require(model.rows.first { $0.id == "\(Ident.lane2)/index.md" })
        #expect(broken.defect.reason.description.contains("unparseable YAML"))
        // Reveal points at the file that is actually there; Open in Editor too.
        #expect(broken.revealURL.lastPathComponent == BoardLoader.indexFileName)
        #expect(broken.editURL != nil)
    }

    @Test("A root with no index.md has nothing to open, and reveals its folder")
    func aMissingIndexRevealsTheFolder() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item(Ident.lane1, "---\nschema: 1\ntitle: Orphan\norder: 1024\n---\n")

        let model = try makeModel(fixture)
        let row = try #require(model.rows.first)

        #expect(row.defectClass == .missingBoardIndex)
        #expect(row.editURL == nil, "Finder cannot open a file that is not there")
        #expect(row.revealURL == fixture.root, "the folder is what the user needs to look at")
    }
}

// MARK: - The choices, the override, and the enabling rule

@MainActor
@Suite("Decision surface ▸ choices and the enabling rule")
struct BoardDecisionSurfaceChoiceTests {

    @Test("A class-level choice sets every row in the class")
    func theClassChoiceSetsTheClass() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("", "---\nschema: 1\ntitle: Board\n---\n")
        try fixture.item(Ident.lane1, "---\nschema: 1\ntitle: A\norder: 1024\nlabels: [a\n---\n")
        try fixture.item(Ident.lane2, "---\nschema: 1\ntitle: B\norder: 2048\nlabels: [b\n---\n")

        let model = try makeModel(fixture)
        #expect(model.sections.first?.classChoice == .editAndRecheck)

        model.choose(.skip, inClass: .unreadableFrontmatter)
        #expect(model.sections.first?.classChoice == .skip)
        #expect(model.skipSet == ["\(Ident.lane1)/index.md", "\(Ident.lane2)/index.md"])
    }

    @Test("A per-item override moves one row and leaves the class mixed")
    func perItemOverrideLeavesTheClassMixed() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("", "---\nschema: 1\ntitle: Board\n---\n")
        try fixture.item(Ident.lane1, "---\nschema: 1\ntitle: A\norder: 1024\nlabels: [a\n---\n")
        try fixture.item(Ident.lane2, "---\nschema: 1\ntitle: B\norder: 2048\nlabels: [b\n---\n")

        let model = try makeModel(fixture)
        model.choose(.skip, forPath: "\(Ident.lane1)/index.md")

        #expect(model.skipSet == ["\(Ident.lane1)/index.md"], "only the overridden row moved")
        #expect(model.sections.first?.classChoice == nil, "the class has no single answer any more")
        #expect(model.sections.first?.offersPerItemOverride == true)
    }

    @Test("A class-level choice a row cannot take leaves that row alone")
    func aRootRowIgnoresAClassLevelSkip() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        // Unparseable YAML at the root *and* below it: one class, two offer sets.
        try fixture.item("", "---\nschema: 1\ntitle: Board\nlabels: [a\n---\n")
        try fixture.item(Ident.lane1, "---\nschema: 1\ntitle: A\norder: 1024\nlabels: [b\n---\n")

        let model = try makeModel(fixture)
        model.choose(.skip, inClass: .unreadableFrontmatter)

        #expect(model.skipSet == ["\(Ident.lane1)/index.md"], "the root never joins a skip set")
        let rootRow = try #require(model.rows.first { $0.id == BoardLoader.indexFileName })
        #expect(rootRow.choice == .editAndRecheck)
    }

    @Test("Repair and Open enables only when every defect has an actionable resolution")
    func repairAndOpenNeedsEveryDefectResolved() throws {
        let fixture = try makeSkippableBoard()
        defer { fixture.tearDown() }
        let model = try makeModel(fixture)

        // Out of the box the unparseable lane defaults to "fix it myself", which is not a resolution.
        #expect(!model.canRepairAndOpen)

        model.choose(.skip, inClass: .unreadableFrontmatter)
        #expect(model.canRepairAndOpen, "a consented Skip is an actionable resolution")
        #expect(model.skipSet.count == 2, "the newer-schema card defaults to Skip, its only offer")
    }

    @Test("Repair and Open enables on a board whose only defect has a minted repair")
    func aMintedRepairAloneEnablesIt() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("", "---\ntitle: No Schema\n---\n")

        let model = try makeModel(fixture)
        #expect(model.canRepairAndOpen)
        #expect(model.plannedRepairs == [
            BoardDecisionSurfaceModel.PlannedRepair(path: "index.md", repair: .stampSchema)
        ])
    }

    @Test("A root blocked by a newer schema disables Repair and Open outright")
    func aRootBlockerDisablesRepairAndOpen() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        // "on the board root it blocks the whole board (Cancel is the only exit)".
        try fixture.item("", "---\nschema: 99\ntitle: From The Future\n---\n")
        try fixture.item(Ident.lane1, "---\nschema: 1\ntitle: Fine\norder: 1024\n---\n")

        let model = try makeModel(fixture)
        let row = try #require(model.rows.first)

        #expect(row.isBlocking, "no repair, and no tolerance at the root")
        #expect(row.choice == nil)
        #expect(model.isRootBlocked)
        #expect(!model.canRepairAndOpen)
    }

    @Test("The surface still shows everything a blocked board has wrong")
    func aBlockedSurfaceStillShowsEverything() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("", "---\nschema: 99\ntitle: From The Future\n---\n")
        try fixture.item(Ident.lane1, "---\nschema: 1\ntitle: A\norder: 1024\nlabels: [a\n---\n")

        let model = try makeModel(fixture)

        // The user whose root was written by a newer Lanework is owed the whole picture — what they
        // do next depends on how much else is wrong.
        #expect(model.sections.map(\.defectClass) == [.newerSchema, .unreadableFrontmatter])
        #expect(model.rows.count == 2)
    }
}

// MARK: - Re-aggregation

@MainActor
@Suite("Decision surface ▸ re-aggregation")
struct BoardDecisionSurfaceReaggregationTests {

    @Test("A fresh walk refreshes the same surface and keeps the answers still standing")
    func reaggregationKeepsStandingAnswers() throws {
        let fixture = try makeTwoClassBoard()
        defer { fixture.tearDown() }
        let model = try makeModel(fixture)

        // The user overrides the broken lane to Skip, then repairs the root by hand outside the app.
        model.choose(.skip, inClass: .unreadableFrontmatter)
        try fixture.item("", "---\nschema: 1\ntitle: Two Classes\n---\n")

        model.reaggregate(try failure(of: fixture))

        #expect(model.sections.map(\.defectClass) == [.unreadableFrontmatter], "the root's defect is gone")
        #expect(model.skipSet == ["\(Ident.lane2)/index.md"], "the standing consent survived the re-walk")
        #expect(model.canRepairAndOpen)
    }

    @Test("A defect the walk newly reveals arrives at its class default")
    func newlyRevealedDefectsTakeTheirDefault() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("", "---\nschema: 1\ntitle: Board\n---\n")
        try fixture.item(Ident.lane1, "---\nschema: 1\ntitle: A\norder: 1024\nlabels: [a\n---\n")
        // Hidden behind the broken lane: a broken lane takes its subtree with it.
        try fixture.item("\(Ident.lane1)/\(Ident.card1)", "---\nschema: 2\ntitle: Newer\norder: 1024\n---\n")

        let model = try makeModel(fixture)
        #expect(model.rows.count == 1, "the card under the broken lane is never enumerated")

        // The lane is fixed by hand; Re-check reveals what it was hiding — in the *same* surface.
        model.choose(.skip, forPath: "\(Ident.lane1)/index.md")
        try fixture.item(Ident.lane1, "---\nschema: 1\ntitle: A\norder: 1024\n---\n")
        model.reaggregate(try failure(of: fixture))

        #expect(model.rows.count == 1)
        let revealed = try #require(model.rows.first)
        #expect(revealed.id == "\(Ident.lane1)/\(Ident.card1)/index.md")
        #expect(revealed.choice == .skip, "the newer-schema class's only offer, preselected")
    }
}

// MARK: - The repairs

@MainActor
@Suite("Decision surface ▸ the minted repairs")
struct BoardDecisionSurfaceRepairTests {

    @Test("Stamping schema writes schema: 1, stamps modified, and clears modified-by")
    func stampingSchemaIsAnOrdinaryAppWrite() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("", """
            ---
            title: No Schema
            created: 2026-01-01T09:00:00Z
            modified: 2026-01-01T09:00:00Z
            modified-by: claude
            project: lanework
            ---
            Body text.

            """)

        let model = try makeModel(fixture)
        let outcome = BoardRepairRun.apply(model.plannedRepairs, boardRoot: fixture.root)
        #expect(outcome.failure == nil)

        let text = try fixture.indexText("")
        #expect(text.contains("schema: 1"))
        // "each repaired `index.md` is an ordinary app write (stamps `modified`, clears
        // `modified-by`)".
        #expect(!text.contains("modified-by"))
        #expect(!text.contains("modified: 2026-01-01"))
        // And the round trip is the Writer's own: unknown keys and the body survive.
        #expect(text.contains("project: lanework"))
        #expect(text.contains("Body text."))

        // The board loads now, which is the whole test of a repair.
        #expect(throws: Never.self) { try BoardLoader.load(boardRoot: fixture.root) }
    }

    @Test("Minting a board index creates one titled after the folder, at schema 1")
    func mintingABoardIndexUsesTheFolderName() throws {
        let outer = FileManager.default.temporaryDirectory
            .appendingPathComponent("DecisionMint-\(UUID().uuidString)", isDirectory: true)
        let root = outer.appendingPathComponent("Roadmap.kanban", isDirectory: true)
        try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
        defer { try? FileManager.default.removeItem(at: outer) }
        try Data("---\nschema: 1\ntitle: Todo\norder: 1024\n---\n".utf8)
            .write(to: {
                let lane = root.appendingPathComponent(Ident.lane1, isDirectory: true)
                try? FileManager.default.createDirectory(at: lane, withIntermediateDirectories: true)
                return lane.appendingPathComponent(BoardLoader.indexFileName)
            }())

        let aggregate: BoardLoadFailure
        do throws(BoardLoadFailure) {
            _ = try BoardLoader.load(boardRoot: root)
            Issue.record("a folder with no index.md is not a board")
            return
        } catch {
            aggregate = error
        }
        let model = BoardDecisionSurfaceModel(failure: aggregate, boardRoot: root)
        #expect(model.plannedRepairs.map(\.repair) == [.mintBoardIndex])

        let outcome = BoardRepairRun.apply(model.plannedRepairs, boardRoot: root)
        #expect(outcome.failure == nil)

        // "(folder-name title, `schema: 1`)" — and the extension is not part of the name
        // (01 § Board naming).
        let loaded = try BoardLoader.load(boardRoot: root)
        #expect(loaded.model.title.value == "Roadmap")
        #expect(loaded.model.schema == 1)
        #expect(loaded.model.lanes.count == 1, "the board that was already there is still there")
    }

    @Test("The repair bracket's receipts are all heal-marked")
    func repairReceiptsAreHealMarked() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("", "---\ntitle: No Schema\n---\n")

        let model = try makeModel(fixture)
        let outcome = BoardRepairRun.apply(model.plannedRepairs, boardRoot: fixture.root)

        let index = fixture.root.appendingPathComponent(BoardLoader.indexFileName)
        #expect(outcome.ledger.receipt(at: index) != nil, "the write dropped a receipt")
        #expect(
            outcome.ledger.isHeal(at: index),
            "01: repairs drop heal-marked receipts and commit separately as one repair commit"
        )
    }

    @Test("A repair pass with nothing to repair writes nothing and fails nothing")
    func anEmptyRepairPassIsANoOp() throws {
        let fixture = try makeSkippableBoard()
        defer { fixture.tearDown() }
        let model = try makeModel(fixture)
        model.choose(.skip, inClass: .unreadableFrontmatter)

        // Every resolution here is a Skip: Repair and Open is a re-walk and nothing else.
        #expect(model.plannedRepairs.isEmpty)
        let outcome = BoardRepairRun.apply(model.plannedRepairs, boardRoot: fixture.root)
        #expect(outcome.failure == nil)
        #expect(outcome.ledger.outstandingReceipts == 0)
    }

    @Test("A repaired board walks clean, which is the loop Repair and Open runs")
    func repairThenWalkIsTheLoop() throws {
        let fixture = try makeTwoClassBoard()
        defer { fixture.tearDown() }
        let model = try makeModel(fixture)
        model.choose(.skip, inClass: .unreadableFrontmatter)

        BoardRepairRun.apply(model.plannedRepairs, boardRoot: fixture.root)
        let result = try BoardLoader.load(boardRoot: fixture.root, skipping: model.skipSet)

        #expect(result.model.schema == 1)
        // The skipped lane left with its whole subtree; the intact one stayed.
        #expect(result.model.lanes.map { $0.id.rawValue } == [Ident.lane1])
        #expect(result.warnings.contains(.userSkipped(path: "\(Ident.lane2)/index.md")))
    }

    @Test("An interrupted batch is accepted: what landed stays, and the rest re-aggregates")
    func aPartialRepairReAggregates() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("", "---\ntitle: No Schema\n---\n")

        // A repair aimed at a file that is not there — the shape of a disk that moved underneath.
        let planned = [
            BoardDecisionSurfaceModel.PlannedRepair(path: "index.md", repair: .stampSchema),
            BoardDecisionSurfaceModel.PlannedRepair(
                path: "\(Ident.lane1)/index.md", repair: .stampSchema)
        ]
        let outcome = BoardRepairRun.apply(planned, boardRoot: fixture.root)

        #expect(outcome.failure != nil, "the second repair could not run")
        #expect(try fixture.indexText("").contains("schema: 1"), "the first one still landed")
        // "every intermediate state valid, and a partial repair simply re-aggregates on the next walk".
        #expect(throws: Never.self) { try BoardLoader.load(boardRoot: fixture.root) }
    }
}

// MARK: - The skip channel through the session

@MainActor
@Suite("Decision surface ▸ the skip set rides the session")
struct BoardDecisionSurfaceSkipTests {

    @Test("A skip-carrying open builds a store that retains the set")
    func theStoreRetainsTheSkipSet() async throws {
        let fixture = try makeSkippableBoard()
        defer { fixture.tearDown() }
        let registry = BoardStoreRegistry()
        let skips: Set<String> = ["\(Ident.lane2)/index.md", "\(Ident.lane1)/\(Ident.card2)/index.md"]

        let store = try #require(try await registry.acquireOffMain(fixture.root, skipping: skips))
        defer { registry.release(store) }

        #expect(store.skippedPaths == skips)
        #expect(store.snapshot.lanes.map { $0.id.rawValue } == [Ident.lane1])
        #expect(store.snapshot.lanes.first?.cards.count == 1, "the newer-schema card left too")
    }

    @Test("Every reload of that session passes the same set")
    func reloadsCarryTheSkipSet() async throws {
        let fixture = try makeSkippableBoard()
        defer { fixture.tearDown() }
        let registry = BoardStoreRegistry()
        let skips: Set<String> = ["\(Ident.lane2)/index.md", "\(Ident.lane1)/\(Ident.card2)/index.md"]

        let store = try #require(try await registry.acquireOffMain(fixture.root, skipping: skips))
        defer { registry.release(store) }

        // A foreign filesystem event. Without the retained set this reload walks the still-broken
        // board, fails, and the window carries a breakage banner over the board the user just chose
        // to open.
        store.handleWatcherEvent(.treeChanged(.foreign))
        await store.awaitQuiescence()

        #expect(store.reloadFailure == nil, "the open's consent stands for the session")
        #expect(store.snapshot.lanes.map { $0.id.rawValue } == [Ident.lane1])
    }

    @Test("The walk marks every skip loudly, and the notice is written from those marks")
    func skipsAreLoudlyMarked() throws {
        let fixture = try makeSkippableBoard()
        defer { fixture.tearDown() }
        // Every defect on this board has to be answered before it opens at all — a skip omits the item
        // it names, never its siblings' defects.
        let skips: Set<String> = ["\(Ident.lane1)/\(Ident.card2)/index.md", "\(Ident.lane2)/index.md"]

        let result = try BoardLoader.load(boardRoot: fixture.root, skipping: skips)
        let skipped = result.warnings.compactMap { warning -> String? in
            if case let .userSkipped(path) = warning { path } else { nil }
        }
        // Walk order, like every other list the aggregate produces.
        #expect(skipped == ["\(Ident.lane1)/\(Ident.card2)/index.md", "\(Ident.lane2)/index.md"])

        // The row the opened board carries, values and all.
        let items = skipped.map { RevealTarget(path: $0, url: fixture.root.appendingPathComponent($0)) }
        let center = BannerCenter()
        center.postSkippedOnOpen(items)

        let loss = try #require(center.losses.first)
        #expect(loss.message == "Opened without 2 items — you chose to skip them")
        #expect(loss.reveals.map(\.path) == skipped)
        // Each reveal points at the file the surface's row named, still on disk and untouched.
        #expect(loss.reveals.allSatisfy { FileManager.default.fileExists(atPath: $0.url.path) })
        // "each with Reveal in Finder", as a control the strip can render and Tab can reach.
        let row = BannerRow.loss(loss)
        #expect(row.tone == .warning, "a warning-tone loss row — nothing failed")
        #expect(row.controls.map(\.label) == ["Reveal in Finder", "Dismiss"])
    }

    @Test("A sole skip is named rather than counted")
    func aSoleSkipIsNamed() throws {
        let center = BannerCenter()
        center.postSkippedOnOpen([
            RevealTarget(path: "lane/index.md", url: URL(fileURLWithPath: "/b/lane/index.md"))
        ])
        #expect(center.losses.first?.message == "Opened without 'lane/index.md' — you chose to skip it")
    }

    @Test("Several skips fold to a count, with a reveal for each")
    func severalSkipsFold() throws {
        let items = [
            RevealTarget(path: "a/index.md", url: URL(fileURLWithPath: "/b/a/index.md")),
            RevealTarget(path: "b/index.md", url: URL(fileURLWithPath: "/b/b/index.md")),
            RevealTarget(path: "c/index.md", url: URL(fileURLWithPath: "/b/c/index.md"))
        ]
        let center = BannerCenter()
        center.postSkippedOnOpen(items)

        let loss = try #require(center.losses.first)
        #expect(loss.message == "Opened without 3 items — you chose to skip them")
        // The fold is only safe because "which ones" survives on the row.
        #expect(loss.reveals.count == 3)
    }

    @Test("An open that skipped nothing says nothing")
    func noSkipsNoNotice() {
        let center = BannerCenter()
        center.postSkippedOnOpen([])
        #expect(center.losses.isEmpty)
    }

    @Test("An ordinary loss row still carries no reveal control")
    func ordinaryLossRowsAreUnchanged() throws {
        let center = BannerCenter()
        center.postSkippedFolders(count: 2)
        let loss = try #require(center.losses.first)
        #expect(loss.reveals.isEmpty)
        #expect(BannerRow.loss(loss).controls.map(\.label) == ["Dismiss"])
    }
}

// MARK: - The attendance branch

@MainActor
@Suite("Decision surface ▸ the attendance branch")
struct BoardDecisionSurfaceAttendanceTests {

    private func aggregate(_ reason: BoardLoadError.Reason, path: String = "index.md") -> BoardLoadFailure {
        BoardLoadFailure(BoardLoadError(path: path, reason: reason))
    }

    @Test("A restored open never reaches the surface, whatever is wrong")
    func restoredAlwaysRetires() {
        // "restoration failures keep the retire-to-welcome-row landing … launch never chains dialogs".
        let repairable = aggregate(.missingSchema)
        let handEdit = aggregate(.unparseableYAML(message: "x", line: 1), path: "lane/index.md")
        #expect(BoardWindowHost.landing(for: repairable, origin: .restored) == .retire)
        #expect(BoardWindowHost.landing(for: handEdit, origin: .restored) == .retire)
    }

    @Test("An attended open reaches the surface for anything a repair could touch")
    func attendedDecides() {
        #expect(BoardWindowHost.landing(for: aggregate(.missingSchema), origin: .attended) == .decide)
        #expect(BoardWindowHost.landing(for: aggregate(.boardRootMissingIndex), origin: .attended) == .decide)
        #expect(BoardWindowHost.landing(for: aggregate(.schemaNewerThanApp(found: 9)), origin: .attended) == .decide)
    }

    @Test("An environmental failure retires even when attended")
    func theEnvironmentalCarveOut() {
        // Redesign Gap 87cd782a: there is nothing on disk to repair, so a surface would offer a
        // decision with no choices in it. Welcome's row says the same thing in one line.
        let gone = BoardLoadFailure(
            BoardLoadError(path: ".", reason: .unreadableRoot(message: "no such file or directory")))
        let file = BoardLoadFailure(BoardLoadError(path: ".", reason: .notADirectory))
        #expect(BoardWindowHost.landing(for: gone, origin: .attended) == .retire)
        #expect(BoardWindowHost.landing(for: file, origin: .attended) == .retire)
    }

    @Test("An environmental defect among repairable ones still shows the surface")
    func aMixedAggregateStillDecides() {
        // The carve-out is "nothing to repair", not "one of these is environmental": the rest of the
        // aggregate is still actionable.
        let mixed = BoardLoadFailure([
            BoardLoadError(path: "index.md", reason: .missingSchema),
            BoardLoadError(path: ".", reason: .notADirectory)
        ])
        #expect(BoardWindowHost.landing(for: mixed, origin: .attended) == .decide)
    }

    @Test("The origin rides the pending open, attended by default")
    func theOriginRidesThePendingOpen() throws {
        let folder = FileManager.default.temporaryDirectory
            .appendingPathComponent("DecisionOrigin-\(UUID().uuidString)", isDirectory: true)
        try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
        defer { try? FileManager.default.removeItem(at: folder) }

        let model = AppModel(
            registryStorageURL: folder.appendingPathComponent("board-registry.json"),
            clipboardStagingRoot: folder.appendingPathComponent("Clipboard", isDirectory: true)
        )
        let attended = folder.appendingPathComponent("Attended.kanban", isDirectory: true)
        let restored = folder.appendingPathComponent("Restored.kanban", isDirectory: true)
        // The stash `openBoard(at:origin:)` makes — reached directly, because that method needs a
        // SwiftUI `OpenWindowAction` no test can build (`AppModel.stashPendingOpen`).
        model.stashPendingOpen(for: BoardWindowRef(url: attended), url: attended, origin: .attended)
        model.stashPendingOpen(for: BoardWindowRef(url: restored), url: restored, origin: .restored)

        let attendedClaim = model.claimPendingOpen(for: BoardWindowRef(url: attended))
        #expect(attendedClaim.origin == .attended)
        #expect(attendedClaim.access != nil, "the scope rides the same stash")
        #expect(model.claimPendingOpen(for: BoardWindowRef(url: restored)).origin == .restored)
        // Claiming removes it, and a window that arrived by some other route reads attended with no
        // scope — the safe direction (the worst it can do is offer a repair nobody asked for).
        let second = model.claimPendingOpen(for: BoardWindowRef(url: attended))
        #expect(second.origin == .attended)
        #expect(second.access == nil)
    }
}
