import CoreGraphics
import Foundation
import Testing
@testable import Kanban

/// Unit tests for the board strip's pure geometry (03-board-ui.md § Layout — full visibility and
/// § Lane): the resting division of the window across width units, and the right-edge drag's
/// asymmetric "shadow leads" snap.
///
/// Fixture throughout the snap suites: a frozen standard of 100 and a gap of 12 — so a `step`
/// (`standard + gap`) is 112, and the slot widths are:
///   1× = 100 · 2× = 212 · 3× = 324
/// The tick-up threshold for shadow `k` is `slotWidth(k) + gap` — the far side of the gap trailing
/// that slot — so 1↔2 ticks up at 112 (100 + 12) and 2↔3 at 224 (212 + 12). The tick-down threshold
/// back to `k - 1` is `slotWidth(k - 1) + gap - reentry`, 10pt shy of that same boundary — so 2↔1
/// ticks down at 102 (112 − 10) and 3↔2 at 214 (224 − 10).
///
/// Where the pathfinder's twin suite pinned a hard 1…3 width cap, these pin the two regimes the
/// screen fit divides (03-board-ui.md § Lane, settled 2026-08-08): up to the fit a tick grows the
/// window, past it a tick re-divides the pinned strip, and `allowedRange`'s ceiling is the strip's
/// own capacity rather than either — the width field itself has no cap. The suites below take a
/// screen fit as a *range* where the mechanism is not what is under test, and the re-divide gets its
/// own suite at the foot of the file.

private let standard: CGFloat = 100
private let gap: CGFloat = 12
private let step: CGFloat = standard + gap   // 112
private let reentry: CGFloat = 10

/// A screen that fits three units — the drag's ceiling in most tests below.
private let fitsThree = 1...3

private func slot(_ units: Int) -> CGFloat {
    LaneLayoutMath.slotWidth(units: units, standard: standard, gap: gap)
}

private func snapped(_ liveWidth: CGFloat, current: Int, range: ClosedRange<Int>? = nil) -> Int {
    LaneLayoutMath.snappedUnits(liveWidth: liveWidth, currentUnits: current,
                                standard: standard, gap: gap,
                                allowedRange: range ?? fitsThree, reentry: reentry)
}

// MARK: - Lane fixtures

/// Loads a board whose lanes carry the given `width:` frontmatter values (`nil` writes no key), in
/// the order given — `order` keys make the display order the argument order.
///
/// Built through `BoardLoader` rather than by constructing `Lane` values by hand: `width`'s
/// leniency is a *read-side* rule (01-storage-format.md § Frontmatter), so the only honest way to
/// ask "what does a malformed width display as" is to put the malformed bytes on disk and load
/// them.
/// `collapsed` is the parallel list of `collapsed:` values (`nil` writes no key), padded with `nil`
/// when it is shorter than `widths` — a board with no folded lane passes none at all and reads exactly
/// as it did before the key existed.
private func lanes(widths: [String?], collapsed: [String?] = []) throws -> [Lane] {
    let root = FileManager.default.temporaryDirectory
        .appendingPathComponent("LaneLayoutMathTests-\(UUID().uuidString)", isDirectory: true)
    try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
    defer { try? FileManager.default.removeItem(at: root) }

    try write("---\nschema: 1\ntitle: Board\n---\n", to: root)
    for (index, width) in widths.enumerated() {
        var frontmatter = "---\nschema: 1\ntitle: Lane \(index)\norder: \(1024 * (index + 1))\n"
        if let width {
            frontmatter += "width: \(width)\n"
        }
        if index < collapsed.count, let value = collapsed[index] {
            frontmatter += "collapsed: \(value)\n"
        }
        frontmatter += "---\n"
        let folder = root.appendingPathComponent(UUID().uuidString.lowercased(), isDirectory: true)
        try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
        try write(frontmatter, to: folder)
    }
    return try BoardLoader.load(boardRoot: root).model.lanes
}

private func write(_ text: String, to folder: URL) throws {
    try Data(text.utf8).write(to: folder.appendingPathComponent("index.md"))
}

private func lane(width: String?) throws -> Lane {
    let loaded = try lanes(widths: [width])
    return try #require(loaded.first)
}

// MARK: - The resting layout

@Suite("LaneLayoutMath ▸ the resting division")
struct LaneLayoutStandardWidthTests {

    @Test("The strip divides its width across the units, counting a gap outside each end")
    func standardWidthCountsOuterMargins() {
        // 1000 = 1 lane + 2 outer gaps: (1000 − 24) / 1.
        #expect(LaneLayoutMath.standardWidth(stripWidth: 1000, totalUnits: 1, gap: 12) == 976)
        // Three units: 4 gaps (2 interior + 2 outer) → (1000 − 48) / 3.
        #expect(abs(LaneLayoutMath.standardWidth(stripWidth: 1000, totalUnits: 3, gap: 12) - 952.0 / 3.0) < 0.0001)
        // A wide lane consumes several units of the same division, and the strip still fills
        // exactly: 4 units of standard plus the gaps is the whole strip.
        let standard = LaneLayoutMath.standardWidth(stripWidth: 1000, totalUnits: 4, gap: 12)
        let filled = 4 * standard + 12 * 5
        #expect(abs(filled - 1000) < 0.0001)
    }

    @Test("A slot swallows the interior gaps it spans, so lanes and slots are the same pixels")
    func slotWidthSwallowsInteriorGaps() {
        #expect(slot(1) == 100)
        #expect(slot(2) == 212)   // 2·100 + 1·12
        #expect(slot(3) == 324)   // 3·100 + 2·12
    }

    @Test("Compression is accepted, not floored — only a 1pt floor keeps frames positive")
    func degenerateStripsCompressWithoutAMinimum() {
        // Twenty units in a small window: each lane is a sliver, and that is the design's answer
        // ("the degenerate case is accepted, not floored"), not a scroll bar.
        let squeezed = LaneLayoutMath.standardWidth(stripWidth: 400, totalUnits: 20, gap: 12)
        #expect(squeezed > 0)
        #expect(squeezed < 10)
        // Narrower than its own gaps: still positive, because a zero or negative frame is a
        // rendering bug rather than a design outcome.
        #expect(LaneLayoutMath.standardWidth(stripWidth: 10, totalUnits: 4, gap: 12) == 1)
        #expect(LaneLayoutMath.standardWidth(stripWidth: 0, totalUnits: 1, gap: 12) == 1)
    }

    @Test("An empty strip still has a divisor")
    func zeroUnitsIsTreatedAsOne() {
        #expect(LaneLayoutMath.standardWidth(stripWidth: 1000, totalUnits: 0, gap: 12) == 976)
        #expect(LaneLayoutMath.totalUnits(of: []) == 1)
    }
}

// MARK: - Reading a lane's units

@Suite("LaneLayoutMath ▸ display units")
struct LaneDisplayUnitsTests {

    @Test("A valid width spans that many units")
    func validWidthSpansItsUnits() throws {
        #expect(LaneLayoutMath.displayUnits(of: try lane(width: "3")) == 3)
        #expect(LaneLayoutMath.displayUnits(of: try lane(width: "1")) == 1)
        // The read side coerces where a sensible reading exists, and the layout follows it.
        #expect(LaneLayoutMath.displayUnits(of: try lane(width: "\"2\"")) == 2)
    }

    @Test("A missing, malformed, zero or negative width renders as one unit")
    func leniencyRendersAsOne() throws {
        let missing = try lane(width: nil)
        #expect(missing.width.isMissing)
        #expect(LaneLayoutMath.displayUnits(of: missing) == 1)

        // A fraction or non-numeric text has no integer reading at all — stays `.malformed` on
        // the model (bytes preserved, not corrected) and renders as 1.
        for raw in ["wide", "1.5"] {
            let lane = try lane(width: raw)
            #expect(lane.width.isMalformed, "width: \(raw) should stay malformed rather than coerce")
            #expect(LaneLayoutMath.displayUnits(of: lane) == 1, "width: \(raw) should render as one unit")
        }

        // An exact integer below 1 is a **different** case (01-storage-format.md § Frontmatter,
        // "ranges are part of the sensible reading", settled): it coerces to `.valid(1)`, not
        // malformed — same on-screen result, different model reading.
        for raw in ["0", "-3"] {
            let lane = try lane(width: raw)
            #expect(lane.width == .valid(1), "width: \(raw) should coerce to 1, not stay malformed")
            #expect(LaneLayoutMath.displayUnits(of: lane) == 1, "width: \(raw) should render as one unit")
        }
    }

    @Test("The strip's unit total is the sum over the lanes it is given")
    func totalUnitsSumsDisplayUnits() throws {
        let loaded = try lanes(widths: ["2", nil, "3", "banana"])
        #expect(loaded.map(LaneLayoutMath.displayUnits(of:)) == [2, 1, 3, 1])
        #expect(LaneLayoutMath.totalUnits(of: loaded) == 7)
        #expect(LaneLayoutMath.totalUnits(of: Array(loaded.prefix(2))) == 3)
    }

    @Test("There is no upper cap on a lane's width")
    func widthIsUncapped() throws {
        // 03-board-ui.md § Lane: "1×, 2×, 3×, … — no cap". A wide lane simply takes more of the
        // division; nothing clamps it on the way in.
        let wide = try lane(width: "40")
        #expect(LaneLayoutMath.displayUnits(of: wide) == 40)
        #expect(LaneLayoutMath.totalUnits(of: [wide]) == 40)
    }

    @Test("The trash's one fixed unit joins the total only while it is shown")
    func trashUnitJoinsTheDivision() throws {
        // 03-board-ui.md § Trash: the quasi-lane "spans a fixed one width unit … consumed only
        // while shown", and Show/Hide Trash is therefore a re-divide trigger — the window is never
        // touched, the same width simply divides across one more unit.
        let loaded = try lanes(widths: ["2", nil, "3"])
        #expect(LaneLayoutMath.totalUnits(of: loaded) == 6)
        #expect(LaneLayoutMath.totalUnits(of: loaded, trashUnits: 1) == 7)

        // Shown on a zero-lane board it is the whole division, not a second unit alongside the
        // empty board's floor of one.
        #expect(LaneLayoutMath.totalUnits(of: [], trashUnits: 1) == 1)
    }
}

// MARK: - Hit testing

/// `laneIndex(atX:…)` — the strip's half of drag-to-restore (03-board-ui.md § Trash). Same lane
/// geometry as the layout above: standard 100, gap 12, so with lanes of 1× and 2× units the slots
/// run [12, 112), [124, 336) and everything past 348 is the trash's side of the strip.
@Suite("LaneLayoutMath ▸ hit testing")
struct LaneHitTestingTests {

    private let units = [1, 2, 1]
    private func hit(_ x: CGFloat) -> Int? {
        LaneLayoutMath.laneIndex(atX: x, unitCounts: units, standard: 100, gap: 12)
    }

    @Test("A point inside a lane's slot names that lane, width counted in units")
    func insideALaneSlot() {
        #expect(hit(12) == 0)
        #expect(hit(111.9) == 0)
        #expect(hit(124) == 1)
        // The 2× lane swallows the interior gap it spans, so its slot runs 212pt, not 200.
        #expect(hit(335.9) == 1)
        #expect(hit(348) == 2)
        #expect(hit(447.9) == 2)
    }

    @Test("The margins, the gaps, and everything past the last lane name nothing")
    func gapsAndMarginsAreNotLanes() {
        // The outer margin, before the first lane.
        #expect(hit(0) == nil)
        #expect(hit(11.9) == nil)
        // The inter-lane gaps.
        #expect(hit(112) == nil)
        #expect(hit(123.9) == nil)
        #expect(hit(336) == nil)
        // Past the last lane — which is exactly where the trash quasi-lane sits, so a row dropped
        // back into the trash writes nothing.
        #expect(hit(448) == nil)
        #expect(hit(10_000) == nil)
        // A negative x (the pointer dragged off the leading edge) is not a lane either.
        #expect(hit(-5) == nil)
    }

    @Test("An empty strip has no lane under any point")
    func emptyStrip() {
        #expect(LaneLayoutMath.laneIndex(atX: 50, unitCounts: [], standard: 100, gap: 12) == nil)
    }
}

// MARK: - The snap

@Suite("LaneLayoutMath ▸ the drag's snap")
struct LaneSnapTests {

    // MARK: Tick up — fires just past the far side of the gap, not inside it

    @Test("Ticks up only past the far side of the trailing gap")
    func ticksUpOnlyPastTheFarSideOfTheGap() {
        // Slot 1 is 100; its trailing gap runs to 112 (100 + 12). The shadow must not lead the live
        // edge while the edge is still IN the gap.
        #expect(snapped(111.9, current: 1) == 1)   // still inside the gap holds
        #expect(snapped(112, current: 1) == 1)     // exactly at the far edge holds (strict >)
        #expect(snapped(112.1, current: 1) == 2)   // clearing the gap ticks up immediately
    }

    @Test("Ticks up by exactly one per call")
    func ticksUpByExactlyOne() {
        // A live width well into 3×'s slot still steps only one unit per call (the continuous drag
        // calls this on every event, and the session iterates it to a fixed point).
        #expect(snapped(1000, current: 1) == 2)
        #expect(snapped(1000, current: 2) == 3)
    }

    // MARK: Tick down — fires only 10pt back inside the gap just crossed

    @Test("Ticks down only below the 10pt re-entry point")
    func ticksDownOnlyBelowTheReentryPoint() {
        // Coming back from 2×, the boundary is slot(1) + gap = 112; tick-down requires retreating a
        // further 10pt to 102.
        #expect(snapped(102.1, current: 2) == 2)   // just above the re-entry point holds
        #expect(snapped(102, current: 2) == 2)     // exactly at the re-entry point holds (strict <)
        #expect(snapped(101.9, current: 2) == 1)   // clearing the re-entry point ticks down
    }

    @Test("Re-entering the gap is not enough to tick down")
    func ticksDownDoesNotFireWhileStillInTheGap() {
        // Immediately after re-entering the gap (e.g. 110), the edge has NOT yet retreated the full
        // 10pt, so the shadow must still hold at 2× — this is the asymmetry: growing was instant,
        // shrinking is not.
        #expect(snapped(110, current: 2) == 2)
        #expect(snapped(103, current: 2) == 2)
    }

    // MARK: Hold-band stability from both directions

    @Test("The hold band is stable from both directions")
    func holdBandIsStable() {
        // The hold band for shadow 2× is (102, 224] — everything strictly between the 1↔2 re-entry
        // point and the 2↔3 tick-up threshold holds at 2× with no oscillation.
        for width in stride(from: CGFloat(103), through: 223, by: 20) {
            #expect(snapped(width, current: 2) == 2, "2× holds at \(width)")
        }
    }

    @Test("A value that just ticked up does not immediately tick back down")
    func noImmediateReversalAfterTickingUp() {
        // Crossing 112.1 ticks 1× → 2×. Re-evaluating at that same live width with the NEW unit
        // count must hold, not bounce back — 112.1 is comfortably above the 2↔1 re-entry point.
        let justTicked = snapped(112.1, current: 1)
        #expect(justTicked == 2)
        #expect(snapped(112.1, current: justTicked) == 2)
    }

    @Test("A value that just ticked down does not immediately tick back up")
    func noImmediateReversalAfterTickingDown() {
        let justTicked = snapped(101.9, current: 2)
        #expect(justTicked == 1)
        #expect(snapped(101.9, current: justTicked) == 1)
    }

    // MARK: Range clamping

    @Test("The snap never steps past the allowed range")
    func neverTicksPastTheAllowedRange() {
        #expect(snapped(5000, current: 3) == 3, "the range's ceiling holds")
        #expect(snapped(0, current: 1) == 1, "one unit is the floor")
        #expect(snapped(-500, current: 1) == 1)
    }

    @Test("The range's ceiling is the only ceiling, and it is not a width cap")
    func snapRespectsTheAllowedCeiling() {
        // With the range capped at 2×, no live width ticks to 3×.
        let capsAtTwo = 1...2
        #expect(snapped(1000, current: 2, range: capsAtTwo) == 2)
        #expect(snapped(5000, current: 2, range: capsAtTwo) == 2)
        // Below the cap it still ticks normally.
        #expect(snapped(200, current: 1, range: capsAtTwo) == 2)
        // A roomier ceiling keeps ticking well past the pathfinder's old 3× — Lanework's width has
        // no cap of its own (03-board-ui.md § Lane).
        #expect(snapped(5000, current: 3, range: 1...9) == 4)
        #expect(snapped(5000, current: 8, range: 1...9) == 9)
    }

    // MARK: maxUnits — the boundary, not the ceiling

    @Test("maxUnits turns window headroom into whole growable units")
    func maxUnitsFromHeadroom() {
        // Two whole steps of headroom (2 × 112 = 224) → grow up to +2.
        #expect(LaneLayoutMath.maxUnits(currentUnits: 1, headroom: 250, step: step) == 3)
        // Just over one step → +1.
        #expect(LaneLayoutMath.maxUnits(currentUnits: 1, headroom: 120, step: step) == 2)
        // Less than a step → no window growth left, so the drag re-divides from its very first
        // tick; shrinking stays allowed either way.
        #expect(LaneLayoutMath.maxUnits(currentUnits: 1, headroom: 50, step: step) == 1)
        #expect(LaneLayoutMath.maxUnits(currentUnits: 2, headroom: 0, step: step) == 2)
        // Never below currentUnits even with a negative headroom (a window already past the visible
        // frame) — shrinking is always allowed.
        #expect(LaneLayoutMath.maxUnits(currentUnits: 2, headroom: -300, step: step) == 2)
        // No width ceiling to clamp against: a huge screen means a huge fit. The pathfinder capped
        // this at 3.
        #expect(LaneLayoutMath.maxUnits(currentUnits: 1, headroom: 10_000, step: step) == 90)
        // A degenerate step is not divided by.
        #expect(LaneLayoutMath.maxUnits(currentUnits: 2, headroom: 500, step: 0) == 2)
    }

    // MARK: Rubber-band resistance

    @Test("Inside the bounds the live width passes through untouched")
    func widthPassesThroughInsideBounds() {
        #expect(LaneLayoutMath.resistedWidth(proposed: 200, minSlot: slot(1),
                                             maxSlot: slot(3), resistance: 0.25) == 200)
        #expect(LaneLayoutMath.resistedWidth(proposed: slot(1), minSlot: slot(1),
                                             maxSlot: slot(3), resistance: 0.25) == slot(1))
        #expect(LaneLayoutMath.resistedWidth(proposed: slot(3), minSlot: slot(1),
                                             maxSlot: slot(3), resistance: 0.25) == slot(3))
    }

    @Test("Past either bound the edge gives only a quarter of the overshoot")
    func widthResistsPastEitherBound() {
        // 20 below the 100 floor → 100 − 20·0.25 = 95.
        #expect(LaneLayoutMath.resistedWidth(proposed: 80, minSlot: slot(1),
                                             maxSlot: slot(3), resistance: 0.25) == 95)
        // 76 above the 324 ceiling → 324 + 76·0.25 = 343.
        #expect(LaneLayoutMath.resistedWidth(proposed: 400, minSlot: slot(1),
                                             maxSlot: slot(3), resistance: 0.25) == 343)
    }
}

// MARK: - The drag past the screen

/// The right-edge drag's **second regime** (03-board-ui.md § Lane, settled 2026-08-08): at the
/// screen's visible frame the window stops growing, and each further tick re-divides the now-pinned
/// strip across one more unit instead — the stepper's mechanism, driven by the drag, with the
/// siblings compressing. Before this the tick simply clamped at the fit, so a lane on a maximised
/// window refused to widen at all.
///
/// Fixture: the file's standard of 100 and gap of 12 on a **four-unit strip** — the dragged 1× lane,
/// two more 1× lanes and the shown trash's fixed unit — with two whole steps of screen headroom, so
/// the fit is 3×. The strip is 100·4 + 12·5 = 460 at drag start and 460 + 2·112 = 684 once the window
/// is flush against the screen, which is the width every tick past 3× re-divides.
@Suite("LaneLayoutMath ▸ the drag past the screen")
struct LaneRedivideTests {

    private let startUnits = 1
    private let startTotal = 4
    private let fit = 3
    private let pinned: CGFloat = 684

    private func standardFor(_ units: Int) -> CGFloat {
        LaneLayoutMath.resizeStandard(
            forUnits: units, startUnits: startUnits, startStandard: standard,
            startTotalUnits: startTotal, fittingUnits: fit, gap: gap)
    }

    private func slotFor(_ units: Int) -> CGFloat {
        LaneLayoutMath.slotWidth(units: units, standard: standardFor(units), gap: gap)
    }

    private var ceiling: Int {
        LaneLayoutMath.resizeMaxUnits(
            startUnits: startUnits, startStandard: standard,
            startTotalUnits: startTotal, fittingUnits: fit, gap: gap)
    }

    private func snapped(_ liveWidth: CGFloat, current: Int) -> Int {
        LaneLayoutMath.snappedUnits(liveWidth: liveWidth, currentUnits: current,
                                    slotFor: slotFor, gap: gap,
                                    allowedRange: 1...ceiling, reentry: reentry)
    }

    // MARK: The pinned strip

    @Test("The pinned strip is the drag-start strip plus one step per unit of screen headroom")
    func pinnedStripWidthIsDerivedNotMeasured() {
        #expect(LaneLayoutMath.pinnedStripWidth(
            startUnits: startUnits, startStandard: standard,
            startTotalUnits: startTotal, fittingUnits: fit, gap: gap) == pinned)
        // No headroom at all: the strip is exactly what the frozen standard and the unit total
        // imply, and the re-divide starts from the very first tick.
        #expect(LaneLayoutMath.pinnedStripWidth(
            startUnits: startUnits, startStandard: standard,
            startTotalUnits: startTotal, fittingUnits: startUnits, gap: gap) == 460)
    }

    // MARK: Continuity at the boundary

    @Test("The two regimes meet with no pixel jump")
    func theBoundaryCostsNothing() {
        // Everything up to the fit is the frozen standard: the window took the step, so the
        // division never moved.
        #expect(standardFor(1) == standard)
        #expect(standardFor(2) == standard)
        #expect(standardFor(fit) == standard, "the re-divided standard AT the fit is the frozen one")
        // Past it the same width divides across one more unit, so it can only shrink.
        #expect(standardFor(fit + 1) < standard)
        #expect(abs(standardFor(fit + 1) - 84) < 0.0001)   // (684 − 12·8) / 7
        #expect(standardFor(fit + 2) < standardFor(fit + 1))
    }

    @Test("Past the fit the strip still fills exactly — the width is pinned, the division is not")
    func exactFillSurvivesTheRedivide() {
        for units in (fit + 1)...12 {
            let redivided = standardFor(units)
            // Four boxes stand in the strip — the dragged lane, two 1× lanes and the trash — so
            // there are five gaps: the three between them and the two outer margins.
            let filled = slotFor(units) + 3 * redivided + 5 * gap
            #expect(abs(filled - pinned) < 0.0001, "\(units)× must still fill the pinned strip exactly")
        }
    }

    // MARK: The snap, measured slot by slot

    @Test("A tick past the fit fires on the re-divided slot, and does not double-tick")
    func theRedivideTicksOnceAndSettles() {
        // 3× is the last slot the window pays for: 324 wide, its trailing gap ending at 336.
        let threshold = slotFor(fit) + gap
        #expect(threshold == 336)
        #expect(snapped(threshold, current: fit) == fit, "exactly at the far edge holds (strict >)")

        let ticked = snapped(threshold + 0.1, current: fit)
        #expect(ticked == fit + 1, "the old clamp at the screen fit is gone")

        // 4× is measured against the SMALLER standard the re-divide produced, and its threshold
        // still sits beyond the width that fired the tick — so the session's iteration settles in
        // one step rather than running away up the strip.
        #expect(slotFor(fit + 1) + gap > threshold + 0.1)
        #expect(snapped(threshold + 0.1, current: ticked) == ticked, "no double tick")
        // And it does not immediately reverse either: the tick-down threshold is 10pt back inside
        // the gap it just cleared.
        #expect(snapped(threshold + 0.1, current: ticked) != fit)
    }

    @Test("Ticking back down retreats through the same re-divided slots")
    func theRedivideTicksBackDown() {
        // Coming back from 4×, the boundary is slot(3) + gap = 336 and the re-entry point 10pt
        // inside it, at 326 — the same asymmetry as within the fit.
        #expect(snapped(326, current: fit + 1) == fit + 1)
        #expect(snapped(325.9, current: fit + 1) == fit)
        #expect(snapped(330, current: fit + 1) == fit + 1, "re-entering the gap is not enough")
    }

    @Test("The ceiling is the strip's capacity, and units run well past the screen fit")
    func theCeilingIsTheStripsCapacity() {
        // 684 wide with 12pt gaps divides into at most 51 whole units before `standardWidth`'s 1pt
        // floor would break the exact fill — floor((684 − 12) / 13) — and the dragged lane reads
        // that back through the 47 units it added.
        #expect(ceiling == 48)
        #expect(ceiling >= fit)
        #expect(standardFor(ceiling) >= 1)
        #expect(abs(slotFor(ceiling) + 3 * standardFor(ceiling) + 5 * gap - pinned) < 0.0001)
        // One unit further the 1pt floor engages, the division stops being a division, and the
        // strip would overflow — which is precisely why the ceiling sits where it does.
        #expect(standardFor(ceiling + 1) == 1)
        #expect(slotFor(ceiling + 1) + 3 + 5 * gap > pinned)
        // The snap walks all the way there and stops.
        #expect(snapped(5000, current: fit) == fit + 1)
        #expect(snapped(5000, current: ceiling) == ceiling)
    }

    @Test("The ceiling never falls below the screen fit, and a degenerate strip falls back to it")
    func theCeilingFallsBackToTheFit() {
        // Shrinking is always allowed, so the fit is the floor of the ceiling however odd the
        // inputs are — a non-finite standard and a gap that would make the capacity formula
        // meaningless both answer the fit rather than inventing a bound.
        let degenerate: [(CGFloat, CGFloat)] = [(.nan, gap), (.infinity, gap), (standard, -1), (standard, -50)]
        for (brokenStandard, brokenGap) in degenerate {
            #expect(LaneLayoutMath.resizeMaxUnits(
                startUnits: startUnits, startStandard: brokenStandard,
                startTotalUnits: startTotal, fittingUnits: fit, gap: brokenGap) == fit)
        }
        // A start already past the fit (a window hanging off the screen) still cannot be clamped
        // below where it stands.
        #expect(LaneLayoutMath.resizeMaxUnits(
            startUnits: 6, startStandard: .nan,
            startTotalUnits: startTotal, fittingUnits: fit, gap: gap) == 6)
    }

    // MARK: The window's share of a tick

    @Test("The window moves only for the part of a step that fits on screen")
    func theWindowTakesOnlyItsShare() {
        // Wholly inside the fit: every unit is the window's.
        #expect(LaneLayoutMath.resizeWindowDelta(from: 1, to: 3, fittingUnits: fit, step: step) == 2 * step)
        // A flick across the boundary: only the first unit was ever the window's to give.
        #expect(LaneLayoutMath.resizeWindowDelta(from: 2, to: 5, fittingUnits: fit, step: step) == step)
        // Wholly above it: the window is pinned and the re-divide does the whole of the work.
        #expect(LaneLayoutMath.resizeWindowDelta(from: 4, to: 7, fittingUnits: fit, step: step) == 0)
        #expect(LaneLayoutMath.resizeWindowDelta(from: fit, to: fit + 1, fittingUnits: fit, step: step) == 0)
        // Shrinking mirrors it exactly — the step handed back is the one that was taken.
        #expect(LaneLayoutMath.resizeWindowDelta(from: 5, to: 2, fittingUnits: fit, step: step) == -step)
        #expect(LaneLayoutMath.resizeWindowDelta(from: 7, to: 4, fittingUnits: fit, step: step) == 0)
        #expect(LaneLayoutMath.resizeWindowDelta(from: 3, to: 1, fittingUnits: fit, step: step) == -2 * step)
        #expect(LaneLayoutMath.resizeWindowDelta(from: 4, to: 4, fittingUnits: fit, step: step) == 0)
    }
}

// MARK: - Collapsed lanes

/// **The fold's arithmetic** (03-board-ui.md § Lane ▸ Collapsed lanes, settled 2026-08-08): a collapsed
/// lane takes a fixed slim strip and is *not* part of the width re-division — its width comes off the
/// top and what is left divides among the expanded lanes alone.
///
/// The exact-fill identity is the property worth pinning rather than any one number, because it is what
/// "every lane is always on screen" means arithmetically once two kinds of lane share the strip:
/// `C·collapsedWidth + T·standard + (T + C + 1)·gap` is the whole strip, always.
@Suite("LaneLayoutMath ▸ collapsed lanes")
struct LaneCollapseLayoutTests {

    /// A slim strip in this suite. Deliberately not `BoardMetrics`' figure: the math takes it as a
    /// parameter, and a test that read the metric would pin the metric rather than the arithmetic.
    private let strip: CGFloat = 44

    @Test("The reading is lenient: only a real `true` folds a lane")
    func onlyTrueFolds() throws {
        #expect(LaneLayoutMath.isCollapsed(try lane(width: nil)) == false)
        let folded = try lanes(widths: [nil], collapsed: ["true"])
        #expect(LaneLayoutMath.isCollapsed(try #require(folded.first)))

        // An explicit `false`, a quoted word, and a value with no boolean reading at all — three
        // shapes, two readings, and the malformed one is expanded because the default is the absent
        // key's.
        for (raw, expected) in [("false", false), ("\"yes\"", true), ("maybe", false), ("3", false)] {
            let loaded = try lanes(widths: [nil], collapsed: [raw])
            #expect(LaneLayoutMath.isCollapsed(try #require(loaded.first)) == expected,
                    "collapsed: \(raw) should read as \(expected ? "folded" : "expanded")")
        }
    }

    @Test("Collapsed lanes are outside the unit total, so folding one is a re-divide trigger")
    func collapsedLanesLeaveTheUnitTotal() throws {
        let loaded = try lanes(widths: ["2", "3", nil], collapsed: [nil, "true", nil])
        // The folded lane keeps its `width` — that is how expanding restores the lane the user had —
        // and still answers `displayUnits`; it simply contributes none of them to the division.
        #expect(loaded.map(LaneLayoutMath.displayUnits(of:)) == [2, 3, 1])
        #expect(LaneLayoutMath.collapsedCount(of: loaded) == 1)
        #expect(LaneLayoutMath.totalUnits(of: loaded) == 3)
        #expect(LaneLayoutMath.totalUnits(of: loaded, trashUnits: 1) == 4)
    }

    @Test("The strip still fills exactly with both kinds of lane in it")
    func mixedStripsFillExactly() throws {
        let loaded = try lanes(widths: ["2", "3", nil], collapsed: [nil, "true", nil])
        let total = LaneLayoutMath.totalUnits(of: loaded)
        let folded = LaneLayoutMath.collapsedCount(of: loaded)
        let standard = LaneLayoutMath.standardWidth(
            stripWidth: 1000, totalUnits: total, gap: gap,
            collapsedCount: folded, collapsedWidth: strip)

        // 1000 = 3 standards + 1 strip + 5 gaps (4 slots, so 5 gaps counting both margins).
        #expect(abs(standard - (1000 - strip - gap * 5) / 3) < 0.0001)
        let widths = LaneLayoutMath.drawnWidths(of: loaded, standard: standard, gap: gap, collapsedWidth: strip)
        #expect(abs(widths.reduce(0, +) + gap * CGFloat(widths.count + 1) - 1000) < 0.0001)
        // The expanded lanes are their slots; the folded one is the constant, whatever its `width`.
        #expect(abs(widths[0] - LaneLayoutMath.slotWidth(units: 2, standard: standard, gap: gap)) < 0.0001)
        #expect(widths[1] == strip)
        #expect(abs(widths[2] - standard) < 0.0001)
    }

    @Test("Folding a lane widens its siblings without touching the window")
    func foldingRedividesTheRemainder() throws {
        let expanded = try lanes(widths: [nil, nil, nil])
        let folded = try lanes(widths: [nil, nil, nil], collapsed: [nil, nil, "true"])

        let before = LaneLayoutMath.standardWidth(
            stripWidth: 1000, totalUnits: LaneLayoutMath.totalUnits(of: expanded), gap: gap,
            collapsedCount: 0, collapsedWidth: strip)
        let after = LaneLayoutMath.standardWidth(
            stripWidth: 1000, totalUnits: LaneLayoutMath.totalUnits(of: folded), gap: gap,
            collapsedCount: LaneLayoutMath.collapsedCount(of: folded), collapsedWidth: strip)
        #expect(after > before, "the two survivors grow into what the third gave up")
    }

    @Test("A board with every lane folded divides nothing and still answers a positive width")
    func allCollapsedIsTheDegenerateEdge() throws {
        let loaded = try lanes(widths: ["2", "3"], collapsed: ["true", "true"])
        #expect(LaneLayoutMath.collapsedCount(of: loaded) == 2)
        // No expanded unit exists, so the total is the divisor guard rather than a description of
        // anything on screen — and nothing draws with the standard it produces.
        #expect(LaneLayoutMath.totalUnits(of: loaded) == 1)
        let standard = LaneLayoutMath.standardWidth(
            stripWidth: 1000, totalUnits: LaneLayoutMath.totalUnits(of: loaded), gap: gap,
            collapsedCount: 2, collapsedWidth: strip)
        #expect(standard > 0)
        let widths = LaneLayoutMath.drawnWidths(of: loaded, standard: standard, gap: gap, collapsedWidth: strip)
        #expect(widths == [strip, strip])

        // A shown trash column *is* a real unit in that total, and correctly takes the remainder.
        let withTrash = LaneLayoutMath.standardWidth(
            stripWidth: 1000, totalUnits: LaneLayoutMath.totalUnits(of: loaded, trashUnits: 1), gap: gap,
            collapsedCount: 2, collapsedWidth: strip)
        #expect(abs(withTrash - (1000 - 2 * strip - gap * 4)) < 0.0001)

        // The pathological strip stays positive rather than negative, `standardWidth`'s 1pt floor.
        #expect(LaneLayoutMath.standardWidth(
            stripWidth: 10, totalUnits: 1, gap: gap, collapsedCount: 4, collapsedWidth: strip) == 1)
    }

    @Test("A strip with no folded lane reads exactly as it did before the key existed")
    func theDefaultsAreInert() {
        #expect(LaneLayoutMath.standardWidth(stripWidth: 1000, totalUnits: 3, gap: gap)
            == LaneLayoutMath.standardWidth(stripWidth: 1000, totalUnits: 3, gap: gap,
                                            collapsedCount: 0, collapsedWidth: strip))
    }

    @Test("Hit testing walks the drawn widths, so a folded lane's zone is where it is drawn")
    func hitTestingFollowsTheDrawnWidths() throws {
        let loaded = try lanes(widths: [nil, nil, nil], collapsed: [nil, "true", nil])
        let localStandard: CGFloat = 100
        let widths = LaneLayoutMath.drawnWidths(
            of: loaded, standard: localStandard, gap: gap, collapsedWidth: strip)
        #expect(widths == [100, strip, 100])

        // Lane 0 spans [12, 112), the strip [124, 168), lane 2 [180, 280) — gaps answer nil.
        #expect(LaneLayoutMath.laneIndex(atX: 50, widths: widths, gap: gap) == 0)
        #expect(LaneLayoutMath.laneIndex(atX: 118, widths: widths, gap: gap) == nil)
        #expect(LaneLayoutMath.laneIndex(atX: 130, widths: widths, gap: gap) == 1)
        #expect(LaneLayoutMath.laneIndex(atX: 167, widths: widths, gap: gap) == 1)
        #expect(LaneLayoutMath.laneIndex(atX: 200, widths: widths, gap: gap) == 2)
        #expect(LaneLayoutMath.laneIndex(atX: 400, widths: widths, gap: gap) == nil)

        // The unit-count entry point is the same walk, so a board with no folded lane cannot answer
        // differently from one with.
        #expect(LaneLayoutMath.laneIndex(atX: 130, unitCounts: [1, 1, 1], standard: localStandard, gap: gap)
            == LaneLayoutMath.laneIndex(atX: 130, widths: [100, 100, 100], gap: gap))
    }

    @Test("A dragged run's span is its drawn footprint, folded members included")
    func draggedRunsMeasureWhatTheyWillDraw() {
        let localStandard: CGFloat = 100
        // Two dragged lanes, the second folded: 100 + gap + 44 rather than 100 + gap + 100.
        let widths = LaneLayoutMath.drawnWidths(
            units: [1, 3], collapsed: [false, true],
            standard: localStandard, gap: gap, collapsedWidth: strip)
        #expect(widths == [100, strip])
        #expect(DropSlotMath.laneRunSpan(widths: widths, gap: gap) == 100 + gap + strip)

        // A shorter `collapsed` list reads as expanded past its end — what a caller with no fold to
        // report passes (the trashed-lane restore).
        #expect(LaneLayoutMath.drawnWidths(
            units: [1, 2], collapsed: [],
            standard: localStandard, gap: gap, collapsedWidth: strip)
            == [100, LaneLayoutMath.slotWidth(units: 2, standard: localStandard, gap: gap)])
    }
}
