import Foundation
import Testing
@testable import Kanban

/// **Duplicate ids within a board are never tolerated** (01-storage-format.md § Fractal layout ▸
/// Rules, settled; the silent-remint re-ruling of 2026-07-29).
///
/// Two folders can end up carrying one UUID however carefully the app mints them — a user copies a
/// card folder in Finder, an agent duplicates a tree, an archive is unpacked over a board. The
/// snapshot invariant does not bend for any of it: **one occurrence per id, board-wide**, because
/// SwiftUI's `ForEach` does not tolerate two equal ids and a board that crashes on load is the worst
/// failure a files-first app has.
///
/// Two classes, two verdicts:
///
/// - **Case-spelled twins** of one identity are spelling artifacts of the *same* item. The canonical
///   all-lowercase spelling wins where present, else the lexicographically first; the loser is a
///   silent stray — logged, preserved, never rendered, **never reminted** (reminting one would create
///   duplicate content the user never made).
/// - **Content duplicates** — distinct folders carrying the identical name string, necessarily under
///   different parents — are copies. Earlier-occurrence-wins; every later occurrence is withheld from
///   the snapshot and then **reminted by a scheduled heal** (re-ruled 2026-07-29 — the user-gated
///   Repair banner retired: "Lanework owns the board and re-mints identity at will").

// MARK: - Identities

/// UUIDs with hex *letters* in them, because a case-twin fixture needs a name whose spelling can
/// actually change — `Ident.card1` is all digits and uppercases to itself.
private enum Dup {
    /// The canonical, all-lowercase spelling.
    static let lower = "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"
    /// The same identity, shouted.
    static let upper = "AAAAAAAA-BBBB-4CCC-8DDD-EEEEEEEEEEEE"
    /// The same identity again, spelled a third way — and lexicographically *after* `upper`, because
    /// `"B" < "b"` in ASCII.
    static let mixed = "AAAAAAAA-bbbb-4ccc-8ddd-eeeeeeeeeeee"
    /// An unrelated identity, for boards that need a card that is nobody's twin.
    static let other = "fedcba98-7654-4321-8fed-cba987654321"
}

private func occurrence(
    _ path: String,
    _ name: String,
    container: IntegrityRules.IdentityOccurrence.Container = .live,
    title: String? = nil,
    birth: Date? = nil,
    historyRank: Int? = nil
) -> IntegrityRules.IdentityOccurrence {
    IntegrityRules.IdentityOccurrence(
        path: path,
        name: name,
        container: container,
        title: title,
        birth: birth,
        historyRank: historyRank
    )
}

private func date(_ offset: TimeInterval) -> Date {
    Date(timeIntervalSince1970: 1_700_000_000 + offset)
}

// MARK: - The rule

/// `IntegrityRules.dedupe(_:)` is pure — an occurrence list in, two lists of losers out — so the rule
/// is pinned here without a filesystem in the way.
@Suite("Duplicate ids ▸ the dedupe rule")
struct DedupeRuleTests {

    @Test("A board with no repeated identity yields nothing")
    func nothingToDo() {
        let verdict = IntegrityRules.dedupe([
            occurrence(Ident.lane1, Ident.lane1),
            occurrence("\(Ident.lane1)/\(Dup.lower)", Dup.lower),
            occurrence("\(Ident.lane1)/\(Dup.other)", Dup.other),
        ])
        #expect(verdict.duplicates.isEmpty)
        #expect(verdict.caseTwins.isEmpty)
    }

    /// The headline case: one hand copy, two lanes, identical name strings.
    @Test("Two identical spellings under different parents: the later one is withheld")
    func contentDuplicateWithholdsTheLater() {
        let verdict = IntegrityRules.dedupe([
            occurrence(Ident.lane1, Ident.lane1),
            occurrence("\(Ident.lane1)/\(Dup.lower)", Dup.lower, title: "Fix login"),
            occurrence(Ident.lane2, Ident.lane2),
            occurrence("\(Ident.lane2)/\(Dup.lower)", Dup.lower, title: "Fix login"),
        ])

        #expect(verdict.caseTwins.isEmpty, "identical spellings are copies, never spelling artifacts")
        #expect(verdict.duplicates == [
            DuplicateIdentity(
                path: "\(Ident.lane2)/\(Dup.lower)",
                identity: Dup.lower,
                title: "Fix login",
                winner: "\(Ident.lane1)/\(Dup.lower)"
            ),
        ])
    }

    /// Rung two of the ladder outranks rung three: the older folder wins even when the traversal meets
    /// it second.
    @Test("Birth date beats traversal order")
    func birthDateBeatsTraversal() {
        let verdict = IntegrityRules.dedupe([
            occurrence("\(Ident.lane1)/\(Dup.lower)", Dup.lower, birth: date(100)),
            occurrence("\(Ident.lane2)/\(Dup.lower)", Dup.lower, birth: date(0)),
        ])
        #expect(verdict.duplicates.map(\.path) == ["\(Ident.lane1)/\(Dup.lower)"])
        #expect(verdict.duplicates.map(\.winner) == ["\(Ident.lane2)/\(Dup.lower)"])
    }

    @Test("Equal birth dates fall through to traversal order")
    func equalDatesFallThrough() {
        let verdict = IntegrityRules.dedupe([
            occurrence("\(Ident.lane1)/\(Dup.lower)", Dup.lower, birth: date(0)),
            occurrence("\(Ident.lane2)/\(Dup.lower)", Dup.lower, birth: date(0)),
        ])
        #expect(verdict.duplicates.map(\.path) == ["\(Ident.lane2)/\(Dup.lower)"])
    }

    /// **An unreadable date is no comparison at all.** Substituting `.distantPast` for a missing birth
    /// date would silently make the unreadable folder win every comparison it entered.
    @Test("One unreadable birth date drops the comparison to traversal order")
    func oneMissingDateFallsThrough() {
        // The folder *with* the older date is second in traversal; if a `nil` date were read as
        // `.distantPast` the first would win, which is exactly what must not happen.
        let verdict = IntegrityRules.dedupe([
            occurrence("\(Ident.lane1)/\(Dup.lower)", Dup.lower, birth: nil),
            occurrence("\(Ident.lane2)/\(Dup.lower)", Dup.lower, birth: date(-10_000)),
        ])
        #expect(verdict.duplicates.map(\.path) == ["\(Ident.lane2)/\(Dup.lower)"])
    }

    @Test("Neither date readable falls through to traversal order")
    func noDatesFallThrough() {
        let verdict = IntegrityRules.dedupe([
            occurrence("\(Ident.lane1)/\(Dup.lower)", Dup.lower),
            occurrence("\(Ident.lane2)/\(Dup.lower)", Dup.lower),
        ])
        #expect(verdict.duplicates.map(\.path) == ["\(Ident.lane2)/\(Dup.lower)"])
    }

    /// Rung one — once the git seam, which only the git provider ever filled; nothing fills it now
    /// that app-managed git is excised (`strategy/01-git-excision.md`, 2026-08-08), and the ladder
    /// falls through to birth date on every board.
    @Test("Git path history beats birth date")
    func historyBeatsBirthDate() {
        let verdict = IntegrityRules.dedupe([
            occurrence("\(Ident.lane1)/\(Dup.lower)", Dup.lower, birth: date(0), historyRank: 7),
            occurrence("\(Ident.lane2)/\(Dup.lower)", Dup.lower, birth: date(100), historyRank: 3),
        ])
        #expect(verdict.duplicates.map(\.path) == ["\(Ident.lane1)/\(Dup.lower)"])
    }

    /// "The path history already tracks outranks the newcomer" — read literally, and *against* the
    /// birth date, which is the whole reason the rungs are ordered.
    @Test("A tracked path outranks an untracked one whatever the dates say")
    func trackedOutranksUntracked() {
        let verdict = IntegrityRules.dedupe([
            occurrence("\(Ident.lane1)/\(Dup.lower)", Dup.lower, birth: date(0), historyRank: nil),
            occurrence("\(Ident.lane2)/\(Dup.lower)", Dup.lower, birth: date(100), historyRank: 9),
        ])
        #expect(verdict.duplicates.map(\.path) == ["\(Ident.lane1)/\(Dup.lower)"])
    }

    @Test("Equal history ranks fall through to the next rung")
    func equalRanksFallThrough() {
        let verdict = IntegrityRules.dedupe([
            occurrence("\(Ident.lane1)/\(Dup.lower)", Dup.lower, birth: date(100), historyRank: 4),
            occurrence("\(Ident.lane2)/\(Dup.lower)", Dup.lower, birth: date(0), historyRank: 4),
        ])
        #expect(verdict.duplicates.map(\.path) == ["\(Ident.lane1)/\(Dup.lower)"])
    }

    // MARK: The container boundary — rung 0

    /// **The visible card never loses to its own ghost** (01-storage-format.md § Fractal layout ▸
    /// Rules, stated 2026-07-29): the straddle case is a restore done as a *copy* — an ⌥-drag out of
    /// the trash in Finder — where the ghost left behind is genuinely the older folder. Every
    /// age-based rung would withhold the card the user just restored; rung 0 is what stops them being
    /// consulted at all.
    @Test("A live occurrence beats an older trashed one")
    func liveBeatsOlderTrashed() {
        let verdict = IntegrityRules.dedupe([
            occurrence("\(Ident.lane1)/\(Dup.lower)", Dup.lower, birth: date(1000)),
            occurrence(".trash/\(Dup.lower)", Dup.lower, container: .trashed, birth: date(0)),
        ])
        #expect(verdict.duplicates.map(\.path) == [".trash/\(Dup.lower)"])
        #expect(verdict.duplicates.map(\.winner) == ["\(Ident.lane1)/\(Dup.lower)"])
    }

    /// Rung 0 sits **above history too**, not merely above age: an ⌥-drag restore leaves the tracked
    /// path in the trash, so the ghost is both older *and* the one git knows.
    @Test("A live occurrence beats a trashed one git says entered first")
    func liveBeatsTrackedTrashed() {
        let verdict = IntegrityRules.dedupe([
            occurrence("\(Ident.lane1)/\(Dup.lower)", Dup.lower, birth: date(1000), historyRank: nil),
            occurrence(".trash/\(Dup.lower)", Dup.lower, container: .trashed, birth: date(0), historyRank: 1),
        ])
        #expect(verdict.duplicates.map(\.path) == [".trash/\(Dup.lower)"])
    }

    /// Traversal order does not decide it either — a trashed occurrence met *first* still loses.
    @Test("A trashed occurrence met first still loses")
    func trashedMetFirstStillLoses() {
        let verdict = IntegrityRules.dedupe([
            occurrence(".trash/\(Dup.lower)", Dup.lower, container: .trashed),
            occurrence("\(Ident.lane1)/\(Dup.lower)", Dup.lower),
        ])
        #expect(verdict.duplicates.map(\.path) == [".trash/\(Dup.lower)"])
    }

    /// **A group wholly inside the trash falls through to the ordinary ladder**, unchanged: rung 0 only
    /// speaks when the occurrences straddle the boundary.
    ///
    /// The paths here are labels — the flat trash cannot really hold two entries with one name, so this
    /// pins the *rule*'s fall-through rather than a reachable board. What makes it worth pinning is the
    /// mistake it rules out: a container rung implemented as "trashed always loses" rather than as a
    /// comparison would have no answer at all when both sides are trashed.
    @Test("Two trashed occurrences fall through to the age ladder")
    func bothTrashedFallThrough() {
        let verdict = IntegrityRules.dedupe([
            occurrence(".trash/\(Dup.lower)", Dup.lower, container: .trashed, birth: date(1000)),
            occurrence(".trash/nested/\(Dup.lower)", Dup.lower, container: .trashed, birth: date(0)),
        ])
        #expect(verdict.duplicates.map(\.path) == [".trash/\(Dup.lower)"], "the older trashed one wins")
    }

    /// Several live occurrences and a ghost: the ghost is out on rung 0, and the live ones settle it
    /// among themselves on the age rungs.
    @Test("The ghost is out first, then the live occurrences settle it by age")
    func ghostOutThenAgeDecides() {
        let verdict = IntegrityRules.dedupe([
            occurrence("\(Ident.lane1)/\(Dup.lower)", Dup.lower, birth: date(200)),
            occurrence(".trash/\(Dup.lower)", Dup.lower, container: .trashed, birth: date(0)),
            occurrence("\(Ident.lane2)/\(Dup.lower)", Dup.lower, birth: date(100)),
        ])
        #expect(verdict.duplicates.map(\.path) == [
            "\(Ident.lane1)/\(Dup.lower)",
            ".trash/\(Dup.lower)",
        ])
        #expect(verdict.duplicates.map(\.winner) == Array(repeating: "\(Ident.lane2)/\(Dup.lower)", count: 2))
    }

    // MARK: Case twins

    /// The canonical spelling wins **wherever it sits in the traversal** — this is not an
    /// earlier-occurrence question, it is a spelling question.
    @Test("The all-lowercase spelling wins, however late it is met")
    func lowercaseWins() {
        let verdict = IntegrityRules.dedupe([
            occurrence("\(Ident.lane1)/\(Dup.upper)", Dup.upper, birth: date(0)),
            occurrence("\(Ident.lane2)/\(Dup.lower)", Dup.lower, birth: date(100)),
        ])
        #expect(verdict.duplicates.isEmpty, "a spelling artifact is never healed")
        #expect(verdict.caseTwins == [
            CaseTwin(path: "\(Ident.lane1)/\(Dup.upper)", winner: "\(Ident.lane2)/\(Dup.lower)"),
        ])
    }

    @Test("With no lowercase spelling present, the lexicographically first wins")
    func lexicographicallyFirstWins() {
        // "AAAAAAAA-BBBB-…" < "AAAAAAAA-bbbb-…" — uppercase sorts before lowercase in ASCII.
        let verdict = IntegrityRules.dedupe([
            occurrence("\(Ident.lane1)/\(Dup.mixed)", Dup.mixed),
            occurrence("\(Ident.lane2)/\(Dup.upper)", Dup.upper),
        ])
        #expect(verdict.duplicates.isEmpty)
        #expect(verdict.caseTwins == [
            CaseTwin(path: "\(Ident.lane1)/\(Dup.mixed)", winner: "\(Ident.lane2)/\(Dup.upper)"),
        ])
    }

    /// **Mixed groups collapse spelling first, then apply earlier-occurrence-wins to what is left.**
    /// The consequence is deliberate and documented: a copy whose case was *also* hand-changed
    /// degrades to the silent case-twin posture and is never reminted.
    @Test("Three occurrences spanning both classes: spelling collapses first")
    func mixedGroupCollapsesSpellingFirst() {
        let verdict = IntegrityRules.dedupe([
            occurrence("\(Ident.lane1)/\(Dup.lower)", Dup.lower, title: "Fix login", birth: date(0)),
            occurrence("\(Ident.lane2)/\(Dup.lower)", Dup.lower, title: "Fix login", birth: date(100)),
            occurrence("\(Ident.lane3)/\(Dup.upper)", Dup.upper, title: "Fix login", birth: date(200)),
        ])

        // The uppercase copy is a spelling artifact — silent, never reminted…
        #expect(verdict.caseTwins == [
            CaseTwin(path: "\(Ident.lane3)/\(Dup.upper)", winner: "\(Ident.lane1)/\(Dup.lower)"),
        ])
        // …and only the second *canonically spelled* occurrence is healed.
        #expect(verdict.duplicates.map(\.path) == ["\(Ident.lane2)/\(Dup.lower)"])
    }

    /// The winner a case twin is named against is the earlier-occurrence winner, not merely the first
    /// canonically spelled folder the walk met.
    @Test("A case twin's named winner is the group's actual winner")
    func caseTwinNamesTheRealWinner() {
        let verdict = IntegrityRules.dedupe([
            occurrence("\(Ident.lane1)/\(Dup.upper)", Dup.upper),
            occurrence("\(Ident.lane2)/\(Dup.lower)", Dup.lower, birth: date(100)),
            occurrence("\(Ident.lane3)/\(Dup.lower)", Dup.lower, birth: date(0)),
        ])
        #expect(verdict.caseTwins.map(\.winner) == ["\(Ident.lane3)/\(Dup.lower)"])
        #expect(verdict.duplicates.map(\.path) == ["\(Ident.lane2)/\(Dup.lower)"])
    }

    /// **The spelling contest obeys the container preference too.** Without it the canonical-wins rule
    /// would hand the identity to the lowercase ghost in the trash and *skip* the live card as a
    /// spelling artifact — the straddle read backwards, and the one outcome the ruling forbids: the
    /// visible card would vanish while its ghost rendered from the trash.
    ///
    /// The twin stays a **twin**: skipped silently, preserved, never reminted. Only the winner
    /// selection gained the container preference; the spelling-artifacts-stay-silent rule is unchanged.
    @Test("An uppercase live occurrence beats a lowercase trashed twin")
    func liveSpellingBeatsTrashedCanonicalSpelling() {
        let verdict = IntegrityRules.dedupe([
            occurrence("\(Ident.lane1)/\(Dup.upper)", Dup.upper, birth: date(1000)),
            occurrence(".trash/\(Dup.lower)", Dup.lower, container: .trashed, birth: date(0)),
        ])
        #expect(verdict.duplicates.isEmpty, "a spelling artifact is never reminted, trashed or not")
        #expect(verdict.caseTwins == [
            CaseTwin(path: ".trash/\(Dup.lower)", winner: "\(Ident.lane1)/\(Dup.upper)"),
        ])
    }

    /// Among *live* spellings the ordinary rule still decides — the container filter narrows the
    /// candidates, it does not replace canonical-else-lexicographic.
    @Test("Among live spellings, canonical still wins")
    func canonicalStillWinsAmongLiveSpellings() {
        let verdict = IntegrityRules.dedupe([
            occurrence("\(Ident.lane1)/\(Dup.upper)", Dup.upper, birth: date(0)),
            occurrence("\(Ident.lane2)/\(Dup.lower)", Dup.lower, birth: date(1000)),
            occurrence(".trash/\(Dup.mixed)", Dup.mixed, container: .trashed, birth: date(-1000)),
        ])
        #expect(verdict.duplicates.isEmpty)
        #expect(verdict.caseTwins.map(\.path).sorted() == [
            ".trash/\(Dup.mixed)",
            "\(Ident.lane1)/\(Dup.upper)",
        ].sorted())
        #expect(verdict.caseTwins.allSatisfy { $0.winner == "\(Ident.lane2)/\(Dup.lower)" })
    }

    /// A wholly trashed group has no live candidates, so the spelling contest is exactly what it was.
    @Test("A wholly trashed group picks its spelling the old way")
    func whollyTrashedSpellingUnchanged() {
        let verdict = IntegrityRules.dedupe([
            occurrence(".trash/\(Dup.upper)", Dup.upper, container: .trashed, birth: date(0)),
            occurrence(".trash/nested/\(Dup.lower)", Dup.lower, container: .trashed, birth: date(1000)),
        ])
        #expect(verdict.duplicates.isEmpty)
        #expect(verdict.caseTwins == [
            CaseTwin(path: ".trash/\(Dup.upper)", winner: ".trash/nested/\(Dup.lower)"),
        ])
    }

    // MARK: Determinism and shape

    /// **Exactly one survivor per identity, whatever the group looks like** — the invariant the whole
    /// pass exists for, over a deliberately nasty group of five.
    @Test("Every group keeps exactly one occurrence")
    func exactlyOneSurvivor() {
        let occurrences = [
            occurrence("\(Ident.lane1)/\(Dup.lower)", Dup.lower, birth: date(50)),
            occurrence("\(Ident.lane2)/\(Dup.upper)", Dup.upper, birth: date(10)),
            occurrence("\(Ident.lane3)/\(Dup.lower)", Dup.lower, birth: date(20)),
            occurrence("\(Ident.lane4)/\(Dup.mixed)", Dup.mixed, birth: date(0)),
            occurrence(".trash/\(Dup.lower)", Dup.lower, container: .trashed, birth: date(90)),
        ]
        let verdict = IntegrityRules.dedupe(occurrences)

        let lost = Set(verdict.duplicates.map(\.path)).union(verdict.caseTwins.map(\.path))
        #expect(lost.count == 4)
        #expect(occurrences.filter { !lost.contains($0.path) }.map(\.path) == ["\(Ident.lane3)/\(Dup.lower)"])
    }

    /// The output order is the traversal's, not a dictionary's — the notice's subjects and the log's
    /// lines must read in board order, and the same input must produce the same answer every time.
    @Test("Losers come back in traversal order, repeatably")
    func traversalOrderedAndStable() {
        let occurrences = [
            occurrence("\(Ident.lane1)/\(Dup.lower)", Dup.lower),
            occurrence("\(Ident.lane1)/\(Dup.other)", Dup.other),
            occurrence("\(Ident.lane2)/\(Dup.lower)", Dup.lower),
            occurrence("\(Ident.lane2)/\(Dup.other)", Dup.other),
            occurrence("\(Ident.lane3)/\(Dup.lower)", Dup.lower),
        ]
        let expected = [
            "\(Ident.lane2)/\(Dup.lower)",
            "\(Ident.lane2)/\(Dup.other)",
            "\(Ident.lane3)/\(Dup.lower)",
        ]
        for _ in 0..<20 {
            #expect(IntegrityRules.dedupe(occurrences).duplicates.map(\.path) == expected)
        }
    }

    /// Identity comparison is UUID-*value* equality, so an occurrence's `identity` is its name folded
    /// — and that is what the defect carries, never the verbatim spelling.
    @Test("A duplicate's identity is canonical, never the folder's spelling")
    func identityIsCanonical() {
        let verdict = IntegrityRules.dedupe([
            occurrence("\(Ident.lane1)/\(Dup.upper)", Dup.upper),
            occurrence("\(Ident.lane2)/\(Dup.upper)", Dup.upper),
        ])
        #expect(verdict.duplicates.map(\.identity) == [Dup.lower])
    }
}

// MARK: - Fixtures

@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.lane2, Item.rich(order: "2048", title: "Doing"))
    return fixture
}

/// Every identity the snapshot renders, canonically — lanes, their cards, and the trash.
private func renderedIdentities(_ model: BoardModel) -> [String] {
    var identities: [String] = []
    for lane in model.lanes {
        identities.append(IntegrityRules.canonicalIdentity(lane.id.rawValue))
        identities.append(contentsOf: lane.cards.map { IntegrityRules.canonicalIdentity($0.id.rawValue) })
    }
    identities.append(contentsOf: model.trash.map { IntegrityRules.canonicalIdentity($0.id.rawValue) })
    return identities
}

private func setBirth(_ url: URL, _ when: Date) throws {
    try FileManager.default.setAttributes([.creationDate: when], ofItemAtPath: url.path)
}

@MainActor
private final class BracketLog {
    private(set) var begins = 0

    func attach(to store: BoardStore) {
        store.watcherBrackets = (begin: { self.begins += 1 }, end: {})
    }
}

@MainActor
private final class StepLog: HistoryProviding {
    var canUndo = false
    var canRedo = false
    var undoActionName: String?
    var redoActionName: String?
    private(set) var registered: [String] = []

    func register(_ step: HistoryStep) { registered.append(step.name) }
    func undo() {}
    func redo() {}
    func clear() {}
}

// MARK: - Detection, through the real loader

/// Detection is **read-only in the loader** — the walk reports and withholds, the store acts.
@MainActor
@Suite("Duplicate ids ▸ detection")
struct DuplicateIdentityDetectionTests {

    @Test("A card hand-copied into another lane loads once, and the copy is reported")
    func copiedCardLoadsOnce() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let original = try fixture.item("\(Ident.lane1)/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login"))
        let copy = try fixture.item("\(Ident.lane2)/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login"))
        try setBirth(original, date(0))
        try setBirth(copy, date(1000))

        let result = try BoardLoader.load(boardRoot: fixture.root)

        // One occurrence per id, board-wide — the invariant.
        #expect(renderedIdentities(result.model).sorted() == renderedIdentities(result.model).sorted())
        #expect(Set(renderedIdentities(result.model)).count == renderedIdentities(result.model).count)
        #expect(result.model.lanes.first { $0.id.rawValue == Ident.lane1 }?.cards.count == 1)
        #expect(result.model.lanes.first { $0.id.rawValue == Ident.lane2 }?.cards.isEmpty == true)

        #expect(result.duplicateIdentities == [
            DuplicateIdentity(
                path: "\(Ident.lane2)/\(Dup.lower)",
                identity: Dup.lower,
                title: "Fix login",
                winner: "\(Ident.lane1)/\(Dup.lower)"
            ),
        ])
        // Nothing was moved, nothing was written: the loader never writes.
        #expect(fixture.exists("\(Ident.lane2)/\(Dup.lower)/index.md"))
        #expect(try fixture.indexText("\(Ident.lane2)/\(Dup.lower)") == Item.rich(order: "1024", title: "Fix login"))
    }

    /// A healthy board pays nothing for this pass and reports nothing — the gate.
    @Test("A board with no duplicates reports none")
    func healthyBoardIsQuiet() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        try fixture.item("\(Ident.lane1)/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login"))
        try fixture.item("\(Ident.lane2)/\(Dup.other)", Item.rich(order: "1024", title: "Other"))

        let result = try BoardLoader.load(boardRoot: fixture.root)
        #expect(result.defects.isEmpty)
        #expect(result.warnings.isEmpty)
        #expect(result.duplicateIdentities.isEmpty)
    }

    /// **Board-wide spans both containers**: a card in the trash and a card in a lane sharing one id
    /// would put two items with equal ids into one snapshot the moment either was rendered.
    @Test("The trash counts — a lane card and a trash card sharing an id dedupe")
    func trashCounts() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let live = try fixture.item("\(Ident.lane1)/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login"))
        let trashed = try fixture.item(".trash/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login"))
        try setBirth(live, date(0))
        try setBirth(trashed, date(1000))

        let result = try BoardLoader.load(boardRoot: fixture.root)

        #expect(result.model.trash.isEmpty)
        #expect(result.model.lanes.first { $0.id.rawValue == Ident.lane1 }?.cards.count == 1)
        #expect(result.duplicateIdentities.map(\.path) == [".trash/\(Dup.lower)"])
        // The trash's `kind` reading goes with the withheld entry: leaving it would stand as an answer
        // about the *live* card, which is the ambiguity the dedupe exists to remove.
        #expect(result.trashKinds.isEmpty)
    }

    /// **The restore-as-a-copy straddle, end to end** (01-storage-format.md § Fractal layout ▸ Rules,
    /// stated 2026-07-29): the user ⌥-dragged a card out of the trash in Finder, so the ghost left
    /// behind is the *older* folder. Earlier-entrant-wins alone would withhold the very card they just
    /// restored and render its ghost from the trash; the container boundary is the first tie-break
    /// precisely so that cannot happen.
    @Test("A live card outranks its own older ghost in the trash")
    func liveCardOutranksItsGhost() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let live = try fixture.item("\(Ident.lane1)/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login"))
        let ghost = try fixture.item(".trash/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login"))
        try setBirth(live, date(1000))
        try setBirth(ghost, date(0))

        let result = try BoardLoader.load(boardRoot: fixture.root)

        // The restored card renders; the ghost is the withheld one.
        #expect(result.model.lanes.first { $0.id.rawValue == Ident.lane1 }?.cards.count == 1)
        #expect(result.model.trash.isEmpty)
        #expect(result.duplicateIdentities.map(\.path) == [".trash/\(Dup.lower)"])
        #expect(result.duplicateIdentities.map(\.winner) == ["\(Ident.lane1)/\(Dup.lower)"])
    }

    /// **The same preference governs a trashed lane sharing a live lane's UUID** — the ruling says so
    /// explicitly, and it needs no special case: a trashed lane is a `.trash/` entry like any other.
    @Test("A trashed lane loses to the live lane sharing its UUID")
    func trashedLaneLosesToLiveLane() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("", Item.board)
        let live = try fixture.item(Dup.lower, Item.rich(order: "1024", title: "Todo"))
        // A trashed lane: a `.trash/` entry with identity-shaped children of its own.
        let trashed = try fixture.item(".trash/\(Dup.lower)", Item.rich(order: "1024", title: "Todo"))
        try fixture.item(".trash/\(Dup.lower)/\(Dup.other)", Item.rich(order: "1024", title: "Nested"))
        try setBirth(live, date(1000))
        try setBirth(trashed, date(0))

        let result = try BoardLoader.load(boardRoot: fixture.root)

        #expect(result.model.lanes.map { $0.id.rawValue } == [Dup.lower])
        #expect(result.model.trash.isEmpty)
        #expect(result.duplicateIdentities.map(\.path) == [".trash/\(Dup.lower)"])
    }

    /// The case-twin straddle through the real loader: the live card is spelled uppercase, its ghost
    /// canonically. The live card renders, the ghost is a **silent twin** — skipped, preserved, and
    /// never reminted, so no heal is scheduled at all.
    @Test("A live uppercase card outranks its canonically spelled ghost, silently")
    func liveUppercaseBeatsCanonicalGhost() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let live = try fixture.item("\(Ident.lane1)/\(Dup.upper)", Item.rich(order: "1024", title: "Fix login"))
        let ghost = try fixture.item(".trash/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login"))
        try setBirth(live, date(1000))
        try setBirth(ghost, date(0))

        let result = try BoardLoader.load(boardRoot: fixture.root)

        #expect(result.model.lanes.first { $0.id.rawValue == Ident.lane1 }?.cards.count == 1)
        #expect(result.model.trash.isEmpty)
        #expect(result.defects.isEmpty, "a spelling artifact is never work")
        #expect(result.warnings == [
            .caseTwinIgnored(path: ".trash/\(Dup.lower)", winner: "\(Ident.lane1)/\(Dup.upper)"),
        ])
        #expect(fixture.exists(".trash/\(Dup.lower)/index.md"), "preserved verbatim")
    }

    /// **Levels are not identity namespaces.** A lane folder copied *into* a lane becomes a
    /// card-level folder carrying the lane's id — one identity, two folders, two levels.
    @Test("A lane and a card sharing an id dedupe across levels")
    func crossLevelDuplicate() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("", Item.board)
        let lane = try fixture.item(Dup.lower, Item.rich(order: "1024", title: "Todo"))
        try fixture.item(Ident.lane2, Item.rich(order: "2048", title: "Doing"))
        let nested = try fixture.item("\(Ident.lane2)/\(Dup.lower)", Item.rich(order: "1024", title: "Copied lane"))
        try setBirth(lane, date(0))
        try setBirth(nested, date(1000))

        let result = try BoardLoader.load(boardRoot: fixture.root)

        #expect(result.model.lanes.map { $0.id.rawValue }.sorted() == [Dup.lower, Ident.lane2].sorted())
        #expect(result.model.lanes.first { $0.id.rawValue == Ident.lane2 }?.cards.isEmpty == true)
        #expect(result.duplicateIdentities.map(\.path) == ["\(Ident.lane2)/\(Dup.lower)"])
    }

    /// **A withheld lane takes its subtree out of the snapshot** — and its cards are still occurrences
    /// in their own right, so a collision nested inside a losing lane is its own withheld occurrence
    /// with its own remint (the import boundary's finest-grain rule, read for the heal).
    @Test("A withheld lane's cards are still their own occurrences")
    func withheldLaneSubtreeStillCollides() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("", Item.board)
        // `<root>/<L>` (a lane) and `<root>/<other lane>/<L>` (a card) share an identity; the lane
        // loses, so its whole subtree leaves the snapshot.
        try fixture.item(Ident.lane2, Item.rich(order: "2048", title: "Doing"))
        let nested = try fixture.item("\(Ident.lane2)/\(Dup.lower)", Item.rich(order: "1024", title: "Copied lane"))
        let lane = try fixture.item(Dup.lower, Item.rich(order: "1024", title: "Todo"))
        try setBirth(nested, date(0))
        try setBirth(lane, date(1000))
        // The losing lane's own card collides with a card in the surviving lane.
        let insideLoser = try fixture.item("\(Dup.lower)/\(Dup.other)", Item.rich(order: "1024", title: "Nested"))
        let insideWinner = try fixture.item("\(Ident.lane2)/\(Dup.other)", Item.rich(order: "2048", title: "Nested"))
        try setBirth(insideWinner, date(0))
        try setBirth(insideLoser, date(1000))

        let result = try BoardLoader.load(boardRoot: fixture.root)

        #expect(result.model.lanes.map { $0.id.rawValue } == [Ident.lane2])
        // Both the losing lane *and* the collision nested inside it are withheld occurrences, each
        // with its own remint to come.
        #expect(Set(result.duplicateIdentities.map(\.path)) == [
            Dup.lower,
            "\(Dup.lower)/\(Dup.other)",
        ])
    }

    // MARK: Case twins

    /// Case twins are the **tolerate** tier: a warning, not a defect — nothing to heal.
    ///
    /// Placed under different parents deliberately: default APFS is case-insensitive, so two
    /// case-spelled *siblings* cannot exist there at all.
    @Test("A case-spelled twin is a silent stray, not a defect")
    func caseTwinIsSilent() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        try fixture.item("\(Ident.lane1)/\(Dup.upper)", Item.rich(order: "1024", title: "Fix login"))
        try fixture.item("\(Ident.lane2)/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login"))

        let result = try BoardLoader.load(boardRoot: fixture.root)

        #expect(result.defects.isEmpty, "a spelling artifact is never work")
        #expect(result.warnings == [
            .caseTwinIgnored(path: "\(Ident.lane1)/\(Dup.upper)", winner: "\(Ident.lane2)/\(Dup.lower)"),
        ])
        // The canonical spelling renders; the twin is preserved verbatim and never rendered.
        #expect(result.model.lanes.first { $0.id.rawValue == Ident.lane1 }?.cards.isEmpty == true)
        #expect(result.model.lanes.first { $0.id.rawValue == Ident.lane2 }?.cards.count == 1)
        #expect(fixture.exists("\(Ident.lane1)/\(Dup.upper)/index.md"))
    }

    @Test("With no canonical spelling, the lexicographically first renders")
    func lexicographicWinnerRenders() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        try fixture.item("\(Ident.lane1)/\(Dup.mixed)", Item.rich(order: "1024", title: "Fix login"))
        try fixture.item("\(Ident.lane2)/\(Dup.upper)", Item.rich(order: "1024", title: "Fix login"))

        let result = try BoardLoader.load(boardRoot: fixture.root)

        #expect(result.defects.isEmpty)
        #expect(result.model.lanes.first { $0.id.rawValue == Ident.lane2 }?.cards.count == 1)
        #expect(result.warnings.map(\.description) == [
            "\(Ident.lane1)/\(Dup.mixed): case-spelled twin of \(Ident.lane2)/\(Dup.upper), ignored as a spelling artifact",
        ])
    }

    /// Three copies of one card: two are withheld, one renders — and the notice folds.
    @Test("Three occurrences leave one standing")
    func threeCopiesLeaveOne() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        try fixture.item(Ident.lane3, Item.rich(order: "3072", title: "Done"))
        let first = try fixture.item("\(Ident.lane1)/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login"))
        let second = try fixture.item("\(Ident.lane2)/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login"))
        let third = try fixture.item("\(Ident.lane3)/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login"))
        try setBirth(first, date(0))
        try setBirth(second, date(100))
        try setBirth(third, date(200))

        let result = try BoardLoader.load(boardRoot: fixture.root)

        #expect(result.duplicateIdentities.map(\.path) == [
            "\(Ident.lane2)/\(Dup.lower)",
            "\(Ident.lane3)/\(Dup.lower)",
        ])
        #expect(renderedIdentities(result.model).filter { $0 == Dup.lower }.count == 1)
    }
}

// MARK: - The remint

/// The Writer's half — a rename, and nothing else.
@MainActor
@Suite("Duplicate ids ▸ the remint")
struct DuplicateIdentityRemintTests {

    private func board() throws -> (WriterFixture, DuplicateIdentity) {
        let fixture = try makeBoard()
        try fixture.item("\(Ident.lane1)/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login"))
        try fixture.item("\(Ident.lane2)/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login"))
        return (fixture, DuplicateIdentity(
            path: "\(Ident.lane2)/\(Dup.lower)",
            identity: Dup.lower,
            title: "Fix login",
            winner: "\(Ident.lane1)/\(Dup.lower)"
        ))
    }

    /// **A fresh lowercase v4, content untouched, nothing stamped.** The remint is an identity repair,
    /// not an edit — the folder's `index.md` is never opened, so `modified` and `modified-by` are
    /// exactly what they were.
    @Test("It renames to a fresh lowercase v4 and leaves everything else alone")
    func remintsToAFreshName() throws {
        let (fixture, duplicate) = try board()
        defer { fixture.tearDown() }
        let before = try fixture.indexText("\(Ident.lane2)/\(Dup.lower)")

        let minted = try BoardWriter.remintDuplicateIdentity(duplicate, inBoard: fixture.root)

        let fresh = try #require(minted?.rawValue)
        #expect(fresh != Dup.lower)
        #expect(fresh == fresh.lowercased(), "the app emits lowercase")
        #expect(UUID(uuidString: fresh) != nil)
        #expect(!fixture.exists("\(Ident.lane2)/\(Dup.lower)"), "the losing name is gone")
        #expect(try fixture.indexText("\(Ident.lane2)/\(fresh)") == before, "byte-verbatim, no stamp")
        // The winner never moved.
        #expect(fixture.exists("\(Ident.lane1)/\(Dup.lower)/index.md"))
    }

    /// Children travel with the folder — the rename never opens anything inside it.
    @Test("Attachments, strays and nested folders travel with the rename")
    func childrenTravel() throws {
        let (fixture, duplicate) = try board()
        defer { fixture.tearDown() }
        try fixture.file("\(Ident.lane2)/\(Dup.lower)/attachments/shot.png", Data("png".utf8))
        try fixture.file("\(Ident.lane2)/\(Dup.lower)/notes.txt", Data("loose".utf8))

        let fresh = try #require(try BoardWriter.remintDuplicateIdentity(duplicate, inBoard: fixture.root)?.rawValue)

        #expect(try fixture.data("\(Ident.lane2)/\(fresh)/attachments/shot.png") == Data("png".utf8))
        #expect(try fixture.data("\(Ident.lane2)/\(fresh)/notes.txt") == Data("loose".utf8))
    }

    /// **The re-verify, first half** — the folder is gone. Losing the race to a foreign fix is
    /// success, never an error.
    @Test("A duplicate hand-deleted under the write is a no-op")
    func vanishedFolderIsANoOp() throws {
        let (fixture, duplicate) = try board()
        defer { fixture.tearDown() }
        try FileManager.default.removeItem(at: fixture.url("\(Ident.lane2)/\(Dup.lower)"))

        #expect(try BoardWriter.remintDuplicateIdentity(duplicate, inBoard: fixture.root) == nil)
    }

    /// **The re-verify, second half** — the *winner* is gone, so this folder is no longer a duplicate
    /// of anything and reminting it would change an identity for no reason at all.
    @Test("A vanished winner makes the remint a no-op")
    func vanishedWinnerIsANoOp() throws {
        let (fixture, duplicate) = try board()
        defer { fixture.tearDown() }
        try FileManager.default.removeItem(at: fixture.url("\(Ident.lane1)/\(Dup.lower)"))

        #expect(try BoardWriter.remintDuplicateIdentity(duplicate, inBoard: fixture.root) == nil)
        #expect(fixture.exists("\(Ident.lane2)/\(Dup.lower)/index.md"), "left exactly as it was")
    }

    /// Idempotent by the same re-verify: a second attempt on already-repaired work does nothing.
    @Test("Reminting twice remints once")
    func remintingTwiceRemintsOnce() throws {
        let (fixture, duplicate) = try board()
        defer { fixture.tearDown() }

        let fresh = try #require(try BoardWriter.remintDuplicateIdentity(duplicate, inBoard: fixture.root)?.rawValue)
        #expect(try BoardWriter.remintDuplicateIdentity(duplicate, inBoard: fixture.root) == nil)
        #expect(fixture.exists("\(Ident.lane2)/\(fresh)/index.md"))
    }

    /// A case twin is never handed to this call by the loader — but if one were, the winner probe is
    /// what still makes it safe: the twin *is* a second occurrence, so the identity check is on the
    /// canonical value and the rename would be honest. What must not happen is a remint of a folder
    /// whose spelling no longer matches the defect that named it.
    @Test("A folder already reminted under us is not reminted again")
    func alreadyRemintedIsANoOp() throws {
        let (fixture, duplicate) = try board()
        defer { fixture.tearDown() }
        try fixture.moveFolder("\(Ident.lane2)/\(Dup.lower)", to: "\(Ident.lane2)/\(Dup.other)")

        #expect(try BoardWriter.remintDuplicateIdentity(duplicate, inBoard: fixture.root) == nil)
        #expect(fixture.exists("\(Ident.lane2)/\(Dup.other)/index.md"))
    }

    /// A rename is a move as far as provenance goes, and **heal-marked**, because the app started it
    /// on its own — once the receipt that split it into its own commit on git boards; app-managed
    /// git is gone now (`strategy/01-git-excision.md`), but the receipt still exists for whatever
    /// consumes it next.
    @Test("It drops a heal-marked move receipt")
    func dropsAHealMarkedReceipt() throws {
        let (fixture, duplicate) = try board()
        defer { fixture.tearDown() }
        let ledger = EchoLedger()
        let from = fixture.url("\(Ident.lane2)/\(Dup.lower)")

        let fresh = try EchoLedger.$current.withValue(ledger) {
            try BoardWriter.remintDuplicateIdentity(duplicate, inBoard: fixture.root)?.rawValue
        }

        let to = fixture.url("\(Ident.lane2)/\(try #require(fresh))")
        #expect(ledger.receipt(at: from) == .move(from: EchoLedger.key(from), to: EchoLedger.key(to)))
        #expect(ledger.isHeal(at: to))
    }

    /// The fresh name is minted away from **every** identity in the board, not merely from this
    /// parent's children: trading one duplicate for another would be the one unacceptable outcome.
    @Test("The minted name collides with nothing board-wide")
    func mintedNameIsBoardWideUnique() throws {
        let (fixture, duplicate) = try board()
        defer { fixture.tearDown() }
        try fixture.item("\(Ident.lane1)/\(Dup.other)", Item.rich(order: "2048", title: "Other"))
        try fixture.item(".trash/\(Ident.card3)", Item.rich(order: "1024", title: "Trashed"))

        let fresh = try #require(try BoardWriter.remintDuplicateIdentity(duplicate, inBoard: fixture.root)?.rawValue)

        let result = try BoardLoader.load(boardRoot: fixture.root)
        #expect(result.defects.isEmpty, "no duplicate left to find")
        #expect(renderedIdentities(result.model).contains(fresh))
        #expect(Set(renderedIdentities(result.model)).count == renderedIdentities(result.model).count)
    }
}

// MARK: - The scheduled heal

/// The store's half — the six-step engine, a notice, and no consent gate anywhere (re-ruled
/// 2026-07-29: the user-gated Repair banner retired).
@MainActor
@Suite("Duplicate ids ▸ the scheduled heal")
struct DuplicateIdentityHealTests {

    private func store() throws -> (WriterFixture, BoardStore) {
        let fixture = try makeBoard()
        let original = try fixture.item("\(Ident.lane1)/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login"))
        let copy = try fixture.item("\(Ident.lane2)/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login"))
        try setBirth(original, date(0))
        try setBirth(copy, date(1000))
        return (fixture, try BoardStore(rootURL: fixture.root))
    }

    @Test("It remints the withheld occurrence and announces the repair")
    func remintsAndAnnounces() throws {
        let (fixture, store) = try store()
        defer { fixture.tearDown() }
        let brackets = BracketLog()
        brackets.attach(to: store)
        #expect(store.duplicateIdentities.count == 1)

        store.remintDuplicateIdentities()

        #expect(!fixture.exists("\(Ident.lane2)/\(Dup.lower)"))
        #expect(store.banners.losses.map(\.message) == ["Repaired duplicate id — 'Fix login'"])
        #expect(store.banners.oneShots.isEmpty, "nothing failed")
        #expect(brackets.begins == 1, "one bracket — one app-mediated reload, one commit")
    }

    /// The whole point of the re-ruling: the withheld window is **one heal cycle**, not a standing
    /// condition. After the heal, the reload renders both cards under distinct ids.
    @Test("The reload after the heal renders both cards and finds no defect")
    func theWindowClosesAfterOneCycle() async throws {
        let (fixture, store) = try store()
        defer { fixture.tearDown() }

        store.remintDuplicateIdentities()
        store.handleWatcherEvent(.treeChanged(.appMediated))
        await store.awaitQuiescence()

        #expect(store.duplicateIdentities.isEmpty)
        #expect(store.snapshot.lanes.first { $0.id.rawValue == Ident.lane1 }?.cards.count == 1)
        #expect(store.snapshot.lanes.first { $0.id.rawValue == Ident.lane2 }?.cards.count == 1)
        #expect(Set(renderedIdentities(store.snapshot)).count == renderedIdentities(store.snapshot).count)
    }

    @Test("A reload fires it")
    func aReloadFiresIt() async throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let original = try fixture.item("\(Ident.lane1)/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login"))
        let store = try BoardStore(rootURL: fixture.root)
        let copy = try fixture.item("\(Ident.lane2)/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login"))
        try setBirth(original, date(0))
        try setBirth(copy, date(1000))

        store.handleWatcherEvent(.treeChanged(.foreign))
        await store.awaitQuiescence()

        #expect(!fixture.exists("\(Ident.lane2)/\(Dup.lower)"))
        #expect(store.banners.losses.count == 1)
    }

    /// **Defer, never abandon**: a read-only board writes nothing *and remembers nothing*, so the
    /// reload that lifts the lock is the reload that heals.
    @Test("A read-only board defers it")
    func lockDefersIt() throws {
        let (fixture, store) = try store()
        defer { fixture.tearDown() }
        store.enterUnwritableLock(.permissionDenied)

        store.remintDuplicateIdentities()

        #expect(fixture.exists("\(Ident.lane2)/\(Dup.lower)/index.md"))
        #expect(store.banners.losses.isEmpty)
        #expect(store.heals.memo(for: .duplicateIdentity) == nil, "deferred, not remembered")
    }

    /// **A repeat says nothing more**, and it is the disk re-verify rather than the memo that makes it
    /// so: the memo *clears on success* (that is load-bearing — see below), so a second call against a
    /// defect list the reload has not replaced yet does open a bracket. What it must not do is rename
    /// anything again or claim a second repair, and the Writer's re-verify is what guarantees both.
    @Test("A repeat attempt on already-repaired work says nothing more")
    func repeatingIsHarmless() throws {
        let (fixture, store) = try store()
        defer { fixture.tearDown() }

        store.remintDuplicateIdentities()
        let after = try fixture.entryNames(Ident.lane2)
        store.remintDuplicateIdentities()

        #expect(try fixture.entryNames(Ident.lane2) == after, "nothing renamed a second time")
        #expect(store.banners.losses.count == 1, "and nothing claimed twice")
        #expect(store.banners.oneShots.isEmpty)
    }

    /// Success clears the memo, which is what lets a *later* duplicate — the same picture restored by
    /// a foreign undo, a second hand copy — be healed again immediately.
    @Test("Success clears the memo")
    func successClearsTheMemo() throws {
        let (fixture, store) = try store()
        defer { fixture.tearDown() }

        store.remintDuplicateIdentities()

        #expect(store.heals.memo(for: .duplicateIdentity) == nil)
    }

    /// The no-op heal: the duplicate resolved itself between the load and the write. Nothing moves,
    /// nothing is said, nothing fails.
    @Test("A duplicate that vanished before the write is a silent no-op")
    func vanishedDuplicateIsASilentNoOp() throws {
        let (fixture, store) = try store()
        defer { fixture.tearDown() }
        #expect(store.duplicateIdentities.count == 1)
        try FileManager.default.removeItem(at: fixture.url("\(Ident.lane2)/\(Dup.lower)"))

        store.remintDuplicateIdentities()

        #expect(store.banners.losses.isEmpty, "nothing was claimed to have been repaired")
        #expect(store.banners.oneShots.isEmpty)
    }

    /// **Not undoable** (13-native-undo.md, re-ruled 2026-07-29): heals are not gestures, so nothing
    /// enters the stack — and undoing a remint would recreate the duplicate it exists to remove.
    @Test("It registers no undo step")
    func registersNoUndoStep() throws {
        let (fixture, store) = try store()
        defer { fixture.tearDown() }
        let steps = StepLog()
        store.history = steps

        store.remintDuplicateIdentities()

        #expect(steps.registered.isEmpty)
    }

    /// A hand-copied lane's worth of collisions arrive together and heal together, in one bracket,
    /// under one folded notice.
    @Test("Several duplicates heal in one bracket under one folded notice")
    func severalHealTogether() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        for (index, id) in [Dup.lower, Dup.other, Ident.card3].enumerated() {
            let original = try fixture.item("\(Ident.lane1)/\(id)", Item.rich(order: "\(1024 * (index + 1))", title: "Card \(index)"))
            let copy = try fixture.item("\(Ident.lane2)/\(id)", Item.rich(order: "\(1024 * (index + 1))", title: "Card \(index)"))
            try setBirth(original, date(0))
            try setBirth(copy, date(1000))
        }
        let store = try BoardStore(rootURL: fixture.root)
        let brackets = BracketLog()
        brackets.attach(to: store)
        #expect(store.duplicateIdentities.count == 3)

        store.remintDuplicateIdentities()

        #expect(brackets.begins == 1)
        #expect(store.banners.losses.map(\.message) == ["Repaired 3 duplicate ids"])
        let result = try BoardLoader.load(boardRoot: fixture.root)
        #expect(result.defects.isEmpty)
        #expect(result.model.lanes.first { $0.id.rawValue == Ident.lane2 }?.cards.count == 3)
    }

    /// Case twins reach no heal at all: the store's defect stream never carries one, so the engine
    /// rests and the folders stay exactly where they are.
    @Test("A case twin schedules nothing")
    func caseTwinSchedulesNothing() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        try fixture.item("\(Ident.lane1)/\(Dup.upper)", Item.rich(order: "1024", title: "Fix login"))
        try fixture.item("\(Ident.lane2)/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login"))
        let store = try BoardStore(rootURL: fixture.root)
        let brackets = BracketLog()
        brackets.attach(to: store)

        store.remintDuplicateIdentities()

        #expect(brackets.begins == 0, "no work, no bracket")
        #expect(store.banners.losses.isEmpty)
        #expect(fixture.exists("\(Ident.lane1)/\(Dup.upper)/index.md"))
    }

    /// **The straddle healed end to end**: the ghost in the trash is the withheld occurrence, so it is
    /// the ghost that gets reminted — the restored card keeps the identity it was restored with, and
    /// the trash ends up holding an ordinary unrelated entry.
    @Test("The ghost is the one reminted, never the restored card")
    func theGhostIsReminted() throws {
        let fixture = try makeBoard()
        defer { fixture.tearDown() }
        let live = try fixture.item("\(Ident.lane1)/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login"))
        let ghost = try fixture.item(".trash/\(Dup.lower)", Item.rich(order: "1024", title: "Fix login"))
        try setBirth(live, date(1000))
        try setBirth(ghost, date(0))
        let store = try BoardStore(rootURL: fixture.root)

        store.remintDuplicateIdentities()

        #expect(fixture.exists("\(Ident.lane1)/\(Dup.lower)/index.md"), "the restored card never moved")
        #expect(!fixture.exists(".trash/\(Dup.lower)"))
        #expect(store.banners.losses.map(\.message) == ["Repaired duplicate id — 'Fix login'"])

        let result = try BoardLoader.load(boardRoot: fixture.root)
        #expect(result.defects.isEmpty)
        #expect(result.model.lanes.first { $0.id.rawValue == Ident.lane1 }?.cards.count == 1)
        #expect(result.model.trash.count == 1)
        #expect(Set(renderedIdentities(result.model)).count == renderedIdentities(result.model).count)
    }

    /// It rides `runScheduledHeals()` like every other scheduled heal — the reload tail and board open.
    @Test("runScheduledHeals includes it")
    func runScheduledHealsIncludesIt() throws {
        let (fixture, store) = try store()
        defer { fixture.tearDown() }

        store.runScheduledHeals()

        #expect(!fixture.exists("\(Ident.lane2)/\(Dup.lower)"))
        #expect(store.banners.losses.map(\.message) == ["Repaired duplicate id — 'Fix login'"])
    }
}

// MARK: - Phrasing

@Suite("Duplicate ids ▸ phrasing")
struct DuplicateIdentityPhrasingTests {

    /// The design's own sentence, verbatim.
    @Test("One remint names the item")
    func oneRemintNamesTheItem() {
        #expect(BannerCenter.remintedDuplicateIDsMessage(for: ["Fix login"])
            == "Repaired duplicate id — 'Fix login'")
    }

    @Test("An untitled item takes the untitled rendering")
    func untitled() {
        #expect(BannerCenter.remintedDuplicateIDsMessage(for: [nil])
            == "Repaired duplicate id — an untitled item")
    }

    @Test("Several fold to a count")
    func severalFold() {
        #expect(BannerCenter.remintedDuplicateIDsMessage(for: ["Fix login", nil, "Ship it"])
            == "Repaired 3 duplicate ids")
    }

    @Test("Nothing reminted says nothing")
    func nothingSaysNothing() {
        #expect(BannerCenter.remintedDuplicateIDsMessage(for: []) == nil)
    }

    /// It rides the **loss-row** class: warning tone, user-dismissed, never expiring — the
    /// relocation's class, because it is the same kind of event. Emphatically not a condition banner
    /// with a button: that is the surface the 2026-07-29 re-ruling retired.
    @Test("It rides the loss-row class, with no condition and no button")
    @MainActor
    func ridesTheLossClass() {
        let banners = BannerCenter()
        banners.postRemintedDuplicateIDs(["Fix login"])
        #expect(banners.losses.count == 1)
        #expect(banners.oneShots.isEmpty)
        #expect(banners.signposts.isEmpty)
    }

    /// The failure's mirror, in the one-shot vocabulary the Writer's errors reach the strip through.
    @Test("A failed remint says so")
    func failureSaysSo() {
        let error = BoardWriteError(
            operation: .repairDuplicateID(title: "Fix login"),
            path: "/b/lane/card",
            reason: .io(message: "permission denied")
        )
        #expect(BannerCenter.headline(for: error)
            == "Couldn't repair the duplicate id of 'Fix login' — permission denied")
    }

    @Test("An untitled failure stays graceful")
    func untitledFailure() {
        let error = BoardWriteError(
            operation: .repairDuplicateID(title: nil),
            path: "/b/lane/card",
            reason: .io(message: "disk full")
        )
        #expect(BannerCenter.headline(for: error) == "Couldn't repair a duplicate id — disk full")
    }

    /// The Repair verb survives the re-ruling as the operation's own word — 06-history-undo.md keeps
    /// it as the heal commit's name.
    @Test("The operation describes itself with the Repair verb")
    func operationDescribesItself() {
        #expect(WriteOperation.repairDuplicateID(title: "Fix login").description
            == "repair the duplicate id of 'Fix login'")
    }

    /// The remint never opens an `index.md`, so there is no document to enrich its title from — it
    /// arrives already filled in from the load.
    @Test("withTitle leaves it alone")
    func withTitleIsIdentity() {
        #expect(WriteOperation.repairDuplicateID(title: "Fix login").withTitle("Something else")
            == .repairDuplicateID(title: "Fix login"))
    }
}
