import XCTest

/// **Performance sanity on a big board** — 8 lanes × 40 cards (`UITestLaunch.FixtureVariant.large`),
/// measured on launch and on one interaction.
///
/// ### Why the budgets are wall-clock assertions and not baselines
///
/// `XCTMeasureOptions` and `XCTPerformanceMetric` baselines are stored *per device* in the Xcode
/// project's `xcshareddata`, keyed by a machine identifier. They do not travel: a fresh clone, a
/// different Mac, or CI has no baseline at all, so `measure` there records a number and passes
/// unconditionally. A "performance test" that passes unconditionally is not a gate — it is a log
/// line — and this repository has no CI to grow one on.
///
/// So each test does both, and they do different jobs:
///
/// - **The `measure` block records the metric**, so a developer reading the test report can see the
///   distribution and Xcode can offer a baseline locally to whoever wants one.
/// - **The explicit wall-clock assertion is the gate.** It is a real bound, checked everywhere, with
///   no stored state behind it.
///
/// ### The budgets, and what they are budgets for
///
/// They are **generous on purpose** and they are *regression* bounds, not targets. What they have to
/// catch is a change that makes the large board an order of magnitude worse — an O(n²) reflow, a
/// synchronous tree walk on the main thread, a per-card watcher — and what they must never do is fail
/// because a laptop was busy. The numbers below are roughly 5× the measured cost on the development
/// machine, which leaves a slow or loaded machine plenty of room while still failing a tenfold
/// regression.
///
/// | Budget | Value | What it covers |
/// | --- | --- | --- |
/// | `launchBudget` | 30 s | Process start, **materializing 328 folders through `BoardWriter`**, the load, and the first frame with a lane on it |
/// | `interactionBudget` | 5 s | View ▸ Show Trash on a 320-card board: the re-divide, every lane's reflow, and the column arriving |
///
/// The launch budget's dominant term is the fixture *build*, not the app: writing an `index.md` per
/// card takes about two seconds on the development machine (`UITestLargeFixtureBoardTests` measures
/// the same work without a window). That is deliberate — the number stays honest about what a launch
/// on this fixture actually costs — but it is why the budget is not tighter, and why a launch
/// regression shows up here as a big move rather than a small one.
///
/// ### Running these
///
/// Like the rest of `KanbanUITests`: a real, unlocked display and Accessibility automation
/// permission. They are also the slowest tests in the repository — each one builds the large board
/// once per `measure` iteration — so they are excluded from the ordinary run and invoked by name.
/// `KanbanUITests/EndToEndVerification.md` carries the command.
final class LargeBoardPerformanceTests: XCTestCase {

    /// Launch to a board window with the large fixture on it.
    static let launchBudget: TimeInterval = 30

    /// One board-wide interaction: View ▸ Show Trash, which re-divides every lane.
    static let interactionBudget: TimeInterval = 5

    override func setUp() {
        super.setUp()
        continueAfterFailure = false
    }

    /// **Launch-to-board**, measured with `XCTApplicationLaunchMetric` and gated by
    /// `launchBudget`.
    ///
    /// `.manuallyStart`, so the measured span is the launch and the window — not the teardown of the
    /// previous iteration's app, which would otherwise be folded into the number.
    ///
    /// The gate is timed separately, before the measure block, for two reasons: a `measure` body runs
    /// five times by default and asserting inside it would report one failure per iteration, and the
    /// first launch is the one a user experiences (the later ones benefit from a warm dyld cache and
    /// a warm filesystem).
    @MainActor
    func testLargeBoardLaunchPerformance() throws {
        let started = Date()
        let first = XCUIApplication.launchedWithFixtureBoard(.large)
        let elapsed = Date().timeIntervalSince(started)
        first.terminate()

        XCTAssertLessThan(
            elapsed,
            Self.launchBudget,
            "launching onto the large fixture took \(String(format: "%.1f", elapsed))s, over the \(Self.launchBudget)s budget"
        )

        let options = XCTMeasureOptions()
        options.invocationOptions = [.manuallyStart]
        measure(metrics: [XCTApplicationLaunchMetric()], options: options) {
            let app = XCUIApplication()
            app.launchArguments += FixtureBoard.large.launchArguments
            startMeasuring()
            app.launch()
            _ = app.windows[FixtureBoard.large.windowTitle].waitForExistence(timeout: XCUIApplication.uiTimeout)
            stopMeasuring()
            app.terminate()
        }
    }

    /// **One interaction on a full board**: View ▸ Show Trash, which 03-board-ui.md § Trash calls "a
    /// re-divide trigger, a lane add's behaviour exactly" — so every one of the eight lanes
    /// recomputes its width and relays its 40 cards.
    ///
    /// Chosen over select-all because it is *observable*: the column arriving is an element the test
    /// can wait for, so the span measured is the whole interaction rather than the round trip of a
    /// keystroke whose completion nothing announces. Select-all's cost is covered indirectly — it
    /// runs inside the same window, over the same 320 cards, in `SelectionGrammarTests`.
    ///
    /// The app is launched **once**, outside the measurement, and the toggle is measured over and
    /// back: showing and hiding are the same re-divide in two directions, and a pair per iteration is
    /// what leaves the board in the state the next iteration starts from.
    @MainActor
    func testLargeBoardShowTrashLatency() throws {
        let app = XCUIApplication.launchedWithFixtureBoard(.large)
        defer { app.terminate() }

        // The first toggle is timed on its own and is the gate — it is the one that pays for whatever
        // the board has not laid out yet, which is exactly the cost a user feels.
        let started = Date()
        app.clickMenuItem("Show Trash", in: "View")
        XCTAssertTrue(
            app.element(labeled: Phrase.trash).waitForExistence(timeout: XCUIApplication.uiTimeout),
            "the trash column never appeared on the large board"
        )
        let elapsed = Date().timeIntervalSince(started)

        XCTAssertLessThan(
            elapsed,
            Self.interactionBudget,
            "showing the trash on the large board took \(String(format: "%.1f", elapsed))s, over the \(Self.interactionBudget)s budget"
        )

        // And the recorded metric, over the toggle in both directions.
        measure(metrics: [XCTClockMetric()]) {
            app.clickMenuItem("Show Trash", in: "View")
            app.clickMenuItem("Show Trash", in: "View")
        }
    }
}
