import XCTest

/// **The automated half of 10-accessibility.md ▸ Verification**, whose first clause is the reason
/// this file is a set of tests and not a checklist:
///
/// > **Automated audits are test failures**: Xcode's accessibility audit (`performAccessibilityAudit`)
/// > runs in UI tests over every surface — board (trash shown and hidden), card window (Preview, Edit,
/// > raw source), welcome, template chooser, board popover, board settings sheet.
///
/// The last of those is **one surface shorter than the design's sentence** since 2026-08-07: the
/// board settings sheet retired that day (03-board-ui.md ▸ Board settings sheet, marked retired —
/// the 2026-07-31 popover/sheet split reversed), and everything it held now renders inside the board
/// popover itself. So the popover's own audit is where those controls are looked at, and the
/// every-surface claim is satisfied by there being one surface fewer rather than by a test skipping
/// one.
///
/// One test per surface, one audit call each. `performAccessibilityAudit` audits **the app's
/// currently displayed UI** rather than a subtree, so each test's job is entirely navigation: get the
/// surface on screen, then let the audit look at whatever is there.
///
/// ### The settings sheet's audit, and where it went
///
/// The sheet was once gated, and this suite could not reach it: the gate had no test control, and a
/// launch argument that would have opened it was rejected as a bypass anyone could type into
/// Terminal (`UITestLaunch` ships in the app binary on purpose). The gate is gone — there is one
/// version of the app and nothing to unlock (12-editions.md ▸ PIVOT 2026-08-08) — and the sheet
/// itself retired on 2026-08-07, taking the test with it. What remains is the precedent: a launch
/// argument never exists to open something the shipped app would refuse. `testBoardInfoPopover`
/// opens the popover onto its Info tab, which the audit covers.
///
/// The popover carried a fourth tab, Git, until the git excision (strategy/01-git-excision.md,
/// 2026-08-08) removed it; the popover is Info, Theme, Sync now, and there is no tab-specific audit
/// left to file.
///
/// ### No waiving
///
/// The audit's issue handler is where a false positive would be excused, and every test here passes
/// `nil` — no handler, nothing excused. That is the design's own posture ("violations are test
/// failures, not warnings"), and it is worth keeping literal: a handler that swallowed one issue type
/// app-wide would also swallow the next real one of that type, on a surface nobody was thinking about
/// when the waiver was written. Should a genuine platform false positive ever need excusing, it goes
/// in as a closure that matches **that one element on that one surface** and carries the reason in a
/// comment beside it — never a bare `return true`.
///
/// ### `.all`, not a narrowed set
///
/// `XCUIAccessibilityAuditType.all` is the default and stays the default. The narrower types
/// (`.contrast`, `.elementDetection`, `.hitRegion`, `.sufficientElementDescription`, `.textClipped`,
/// `.trait`) each map onto a rule 10-accessibility.md states — contrast is its ≥ 4.5:1 clause,
/// sufficient-description is its labels, trait is its selection and heading traits — so scoping any
/// of them out would be scoping out a design rule. They are named here only so a future narrowing has
/// to argue with this paragraph first.
///
/// ### The fixture board
///
/// Every test launches with `UITestLaunch.fixtureFlag`, which makes the app build a known board
/// inside its own container and open it (see `UITestLaunch` for why the board cannot simply be
/// handed over on the command line — the sandbox). The board is three lanes, six cards, one card
/// with a rich Markdown body and an attachment, and one card already in `.trash/`. That is the
/// `standard` fixture variant; the launch helpers and the other two shapes live in
/// `UITestSupport.swift`.
///
/// ### Running these
///
/// They drive the real app through the real menu bar, so the machine running them must have granted
/// the test runner Accessibility control (System Settings ▸ Privacy & Security ▸ Accessibility) and
/// must not be locked or headless. A run that cannot get automation permission fails on the first
/// `click()`, not on an accessibility defect — see `KanbanUITests/AccessibilityVerification.md`,
/// which puts this suite at the top of the manual pass for exactly that reason.
///
/// ### Reading a failure
///
/// The navigation waits are deliberately loud, and they are also the part most likely to need
/// adjusting: a window is identified by its **title** (`app.windows["…"]`, which is what
/// `navigationTitle` produces) and an on-board element by its **label**. Both are specified —
/// 11-command-nexus.md fixes the menu titles, `AccessibilityPhrases` fixes the labels — but neither
/// says which `XCUIElement.ElementType` SwiftUI will choose, and a hidden-title-bar window (welcome)
/// is the one place a title might not surface at all. So: a failure on `performAccessibilityAudit`
/// is an accessibility defect and is what this suite is for; a failure on a `waitForExistence` above
/// it is a navigation problem in *this file*, and the audit never ran.
final class AccessibilityAuditTests: XCTestCase {

    override func setUp() {
        super.setUp()
        // A failed navigation step makes every later step in that test meaningless — and an audit
        // that ran against the wrong surface would report a *pass*, which is worse than a failure.
        continueAfterFailure = false
    }

    // MARK: - The board window

    /// The board as it opens: lanes, cards, the toolbar, the search field — trash hidden, which is
    /// the board's default state (03-board-ui.md § Trash).
    @MainActor
    func testBoardWindowWithTrashHidden() throws {
        let app = XCUIApplication.launchedWithFixtureBoard()
        try app.performAccessibilityAudit()
    }

    /// The same board with View ▸ Show Trash on — "when shown, it is the last container, labeled as
    /// Trash with its count" (10-accessibility.md ▸ Trash lane), holding the fixture's one trashed
    /// card so the column's own card elements are audited and not just its header.
    @MainActor
    func testBoardWindowWithTrashShown() throws {
        let app = XCUIApplication.launchedWithFixtureBoard()
        app.clickMenuItem("Show Trash", in: "View")
        XCTAssertTrue(
            app.element(labeled: "Trash").waitForExistence(timeout: XCUIApplication.uiTimeout),
            "the trash column did not appear"
        )
        try app.performAccessibilityAudit()
    }

    // MARK: - The card window

    /// Preview mode — the card window's default. The fixture's rich card carries the structures 10
    /// makes claims about (headings, lists, a task list, a table, a code block, a link, an image with
    /// alt text), so this is the audit of "Preview renders to the accessibility tree as structured
    /// text" rather than of an empty body.
    @MainActor
    func testCardWindowPreviewMode() throws {
        let app = XCUIApplication.launchedWithFixtureBoard()
        try app.openRichCardWindow()
        try app.performAccessibilityAudit()
    }

    /// Edit mode — View ▸ Edit Body (⌘E), "an ordinary accessible text editor" (10 ▸ Card window).
    @MainActor
    func testCardWindowEditMode() throws {
        let app = XCUIApplication.launchedWithFixtureBoard()
        try app.openRichCardWindow()
        app.clickMenuItem("Edit Body", in: "View")
        try app.performAccessibilityAudit()
    }

    /// The raw-source outlet — View ▸ Raw Source (⌥⌘E), the whole `index.md` as text.
    ///
    /// Entered from Preview rather than from Edit, because the two are mutually exclusive by design
    /// ("Edit Body disables while Raw Source is active" — 05-card-window.md ▸ Raw source outlet) and
    /// stacking them would be auditing a state the app does not have.
    @MainActor
    func testCardWindowRawSourceMode() throws {
        let app = XCUIApplication.launchedWithFixtureBoard()
        try app.openRichCardWindow()
        app.clickMenuItem("Raw Source", in: "View")
        try app.performAccessibilityAudit()
    }

    /// **The comments pane**, over the fixture's seeded thread (10-accessibility.md ▸ Comments: the
    /// labeled container, the flattened comment elements with their three custom actions, the labeled
    /// composer and the Tab-reachable sort control).
    ///
    /// The pane is on screen already — View ▸ Show Comments is one persisted app-wide bit and its
    /// shipped default is on (05-card-window.md ▸ The comments column) — so this test's navigation is
    /// the card window's, plus a wait on the container's own label to prove the pane rendered rather
    /// than auditing a body column that happened to be alone.
    ///
    /// The thread is three comments, one of them unattributed and one edited (`UITestLaunch`), which
    /// is what makes this an audit of the *rows* rather than of an empty invitation.
    @MainActor
    func testCardWindowCommentsPane() throws {
        let app = XCUIApplication.launchedWithFixtureBoard()
        try app.openRichCardWindow()
        XCTAssertTrue(
            app.element(labeled: Phrase.comments(3)).waitForExistence(timeout: XCUIApplication.uiTimeout),
            "the comments pane did not appear"
        )
        try app.performAccessibilityAudit()
    }

    // MARK: - Welcome, the template chooser, the board popover

    /// The welcome window, reached by its own Window-menu row — and reached *after* the fixture board
    /// has opened, so its recents list has a row in it. An empty welcome would audit the empty state
    /// and miss the rows 10 specifies ("⟨name⟩, ⟨location⟩, N lanes, M cards").
    @MainActor
    func testWelcomeWindow() throws {
        let app = XCUIApplication.launchedWithFixtureBoard()
        app.clickMenuItem("Welcome to Lanework", in: "Window")
        XCTAssertTrue(
            app.windows["Welcome to Lanework"].waitForExistence(timeout: XCUIApplication.uiTimeout),
            "the welcome window did not appear"
        )
        try app.performAccessibilityAudit()
    }

    /// The template chooser — File ▸ New Board… (⌥⌘N), the ten bundled templates as elements
    /// "labeled by title", their mini previews hidden (10 ▸ Template chooser).
    @MainActor
    func testTemplateChooser() throws {
        let app = XCUIApplication.launchedWithFixtureBoard()
        app.clickMenuItem("New Board…", in: "File")
        XCTAssertTrue(
            app.windows["New Board"].waitForExistence(timeout: XCUIApplication.uiTimeout),
            "the template chooser did not appear"
        )
        try app.performAccessibilityAudit()
    }

    /// The board popover — File ▸ Board Info (⌘I): "labeled controls throughout", with every tab's
    /// information readable as text and never by colour or shape alone (10 ▸ Board popover).
    @MainActor
    func testBoardInfoPopover() throws {
        let app = XCUIApplication.launchedWithFixtureBoard()
        app.clickMenuItem("Board Info", in: "File")
        XCTAssertTrue(
            app.popovers.firstMatch.waitForExistence(timeout: XCUIApplication.uiTimeout),
            "the board popover did not appear"
        )
        try app.performAccessibilityAudit()
    }
}
