import XCTest

/// **The golden paths, driven through the shipping app** — create, rename, move, cut and paste,
/// delete, restore, empty the trash, undo and redo.
///
/// ### What this suite is for, and what it is not
///
/// Every rule these flows exercise is already pinned somewhere in `KanbanTests`: the write paths by
/// `*WriteTests`, the grammar by `SelectionGrammarTests` and `KeyboardGrammarTests`, the trash by
/// `TrashModelTests` and `TrashWriteTests`, the stack by `UndoWriteTests`. Not one assertion below is
/// a fact those suites cannot state faster and more precisely.
///
/// What they cannot state is that the pieces are *wired together* — that File ▸ New Card reaches
/// `beginNewCard`, that the editor it opens is focused, that Return commits it, that the write lands,
/// that the watcher rounds it back, and that the lane the user is looking at now says "4 cards". This
/// suite is that sentence, once per flow, and nothing else. When one of these fails and the unit
/// suites stay green, the defect is in the wiring — a menu item scoped to the wrong focus value, a
/// command that lost its shortcut, a reload that never arrived.
///
/// ### How a flow asserts
///
/// **On lane labels**, almost always (`XCUIApplication.awaitLane`). A lane's accessibility label is
/// "⟨title⟩, lane, N cards" and the count is the *rendered* one, so one label assertion covers the
/// visible badge, the spoken count and the layout at once — 10-accessibility.md ▸ Trash lane makes
/// them one number, and this is where that pays. A card's own element carries only its title, which
/// does not change when the card changes lanes, so asserting on the card would assert nothing about
/// where it went.
///
/// The fixture is the `standard` board: three lanes — **To Do** (3 cards), **Doing** (1), **Done**
/// (1) — plus one card already in `.trash/`. Every flow below starts from exactly that and states
/// its own arithmetic against it.
///
/// ### 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 — the whole of `KanbanUITests/EndToEndVerification.md`'s
/// prerequisites section, which is where the run command lives too.
///
/// ### Reading a failure
///
/// A failure on a `clickMenuItem` is a *validation* failure: the row was missing or disabled, which
/// means a focused value did not reach it. A failure on an `awaitLane` is the flow itself: the
/// command ran and the board did not end up where the design says it should. A failure on a
/// `waitForExistence` for a card or an editor is navigation, in this file.
final class EndToEndFlowTests: XCTestCase {

    override func setUp() {
        super.setUp()
        // A flow is a sequence: once a step has failed, every later assertion is about a board in a
        // state nobody wrote down.
        continueAfterFailure = false
    }

    // MARK: - Creating

    /// **File ▸ New Card (⌘N)** — the placeholder opens focused in the target lane, Return commits,
    /// and the card is on the board (04-interactions.md ▸ Grammar: "Return commits and re-selects the
    /// lane").
    ///
    /// The target is made explicit by selecting a card in "To Do" first. ⌘N with nothing selected
    /// would also land there (`NewCardTarget.resolve` falls back to the first lane), but a flow that
    /// relies on a fallback is a flow that would pass for the wrong reason.
    @MainActor
    func testCreateCard() throws {
        let app = XCUIApplication.launchedWithFixtureBoard()
        app.selectCard(StandardBoard.firstLaneCards[0])

        app.clickMenuItem("New Card", in: "File")
        app.typeIntoInlineEditor("A card the suite made", prompt: Phrase.cardTitlePrompt)

        XCTAssertTrue(
            app.element(labeled: "A card the suite made").waitForExistence(timeout: XCUIApplication.uiTimeout),
            "the new card never appeared on the board"
        )
        app.awaitLane(StandardBoard.toDo, cards: 4, "the new card should have landed in To Do")
    }

    /// **File ▸ New Lane (⇧⌘N)** — a lane with no title and no editor (`BoardStore.createLane`), so
    /// it arrives reading the untitled placeholder with an empty count.
    ///
    /// The count is the assertion that matters: a new lane that arrived holding cards would mean the
    /// create had landed somewhere it should not have.
    @MainActor
    func testCreateLane() throws {
        let app = XCUIApplication.launchedWithFixtureBoard()

        app.clickMenuItem("New Lane", in: "File")

        app.awaitLane(Phrase.untitled, cards: 0, "File ▸ New Lane should add one empty, untitled lane")
        // The three lanes that were already there are untouched — a create is not a re-divide of the
        // board's contents.
        app.awaitLane(StandardBoard.toDo, cards: 3)
        app.awaitLane(StandardBoard.doing, cards: 1)
        app.awaitLane(StandardBoard.done, cards: 1)
    }

    // MARK: - Renaming

    /// **Board ▸ Rename** on a card — the inline editor, seeded with the current title, committed
    /// with Return (04-interactions.md ▸ Selection; the menu row exists "for completeness and
    /// remapping", the pointer path being a slow double click).
    ///
    /// Select All then type is how the seeded title is replaced: inside a focused field that chord
    /// belongs to the field editor, because the board's own Select All disables while an inline
    /// editor is open.
    @MainActor
    func testRenameCardInline() throws {
        let app = XCUIApplication.launchedWithFixtureBoard()
        let original = StandardBoard.firstLaneCards[3]
        let renamed = "Ship the audit, renamed"

        app.selectCard(original)
        app.clickMenuItem("Rename", in: "Board")
        app.typeIntoInlineEditor(renamed, prompt: Phrase.cardTitlePrompt, replacingExisting: true)

        XCTAssertTrue(
            app.element(labeled: renamed).waitForExistence(timeout: XCUIApplication.uiTimeout),
            "the renamed card never appeared"
        )
        // A rename is not a move: the lane still holds the same three cards.
        app.awaitLane(StandardBoard.toDo, cards: 3)
    }

    // MARK: - Moving

    /// **A pointer drag across lanes** (04-interactions.md ▸ Drag and drop) — the card leaves "To Do"
    /// and lands in "Doing", which the two lanes' counts say and nothing else has to.
    ///
    /// This is the suite's one gesture that goes through a real AppKit dragging session rather than
    /// through a menu, and it is correspondingly the most environment-sensitive
    /// (`XCUIApplication.dragCard` carries the details). It earns its place anyway: the drag is the
    /// board's headline interaction, its reflow is the largest piece of geometry in the app, and no
    /// unit test can press a mouse button.
    @MainActor
    func testDragCardBetweenLanes() throws {
        let app = XCUIApplication.launchedWithFixtureBoard()

        app.dragCard(StandardBoard.firstLaneCards[0], onto: StandardBoard.richCard)

        app.awaitLane(StandardBoard.doing, cards: 2, "the dragged card should have landed in Doing")
        app.awaitLane(StandardBoard.toDo, cards: 2, "the dragged card should have left To Do")
    }

    /// **Cut and paste across lanes (⌘X / ⌘V)** — the keyboard's own move (04-interactions.md
    /// ▸ Clipboard), driven through the Edit menu because the board answers `cut:`/`paste:` as a
    /// responder and the menu is where that wiring is visible.
    ///
    /// The paste lands the card **after** the anchor card, which is why the destination is selected
    /// by clicking a card in it rather than by clicking the lane.
    @MainActor
    func testCutAndPasteCardAcrossLanes() throws {
        let app = XCUIApplication.launchedWithFixtureBoard()

        app.selectCard(StandardBoard.firstLaneCards[0])
        app.clickMenuItem("Cut", in: "Edit")
        app.selectCard(StandardBoard.richCard)
        app.clickMenuItem("Paste", in: "Edit")

        app.awaitLane(StandardBoard.doing, cards: 2, "the cut card should have landed in Doing")
        app.awaitLane(StandardBoard.toDo, cards: 2, "the cut card should have left To Do")
    }

    /// **⌘Z and ⇧⌘Z over a move** (13-native-undo.md ▸ Rules) — the chords rather than the menu rows,
    /// because those rows are titled dynamically ("Undo Move Card") by `NSUndoManager` and a test
    /// that spelled the title would be asserting the platform's composition rather than the app's
    /// stack.
    ///
    /// The move is the cut/paste above, so what undo has to put back is a folder that physically
    /// changed lanes — the interesting case, and the one the native provider registers an inverse
    /// for at the Writer boundary.
    @MainActor
    func testUndoAndRedoOfAMove() throws {
        let app = XCUIApplication.launchedWithFixtureBoard()

        app.selectCard(StandardBoard.firstLaneCards[0])
        app.clickMenuItem("Cut", in: "Edit")
        app.selectCard(StandardBoard.richCard)
        app.clickMenuItem("Paste", in: "Edit")
        app.awaitLane(StandardBoard.doing, cards: 2)

        app.typeKey("z", modifierFlags: .command)
        app.awaitLane(StandardBoard.toDo, cards: 3, "⌘Z should have returned the card to To Do")
        app.awaitLane(StandardBoard.doing, cards: 1)

        app.typeKey("z", modifierFlags: [.shift, .command])
        app.awaitLane(StandardBoard.doing, cards: 2, "⇧⌘Z should have moved the card back into Doing")
        app.awaitLane(StandardBoard.toDo, cards: 2)
    }

    // MARK: - The trash

    /// **Delete, then restore** — the whole of the current trash grammar's board side
    /// (03-board-ui.md § Trash, resettled 2026-07-28 for the materialized trash).
    ///
    /// Four claims in one flow, because they are one story:
    ///
    /// 1. **File ▸ Delete on a board card does not confirm.** The card moves into `<root>/.trash/`
    ///    and that is recoverable, so there is no alert — this flow would hang on one if there were.
    /// 2. **View ▸ Show Trash** puts the column on the board as the last container, holding what was
    ///    already in it plus what just arrived.
    /// 3. **Restore is an ordinary move out**: ⌘X in the trash, ⌘V into a lane. There is no Put Back
    ///    and there is nothing else to press.
    /// 4. The counts on both sides move, both times.
    @MainActor
    func testDeleteToTrashAndRestore() throws {
        let app = XCUIApplication.launchedWithFixtureBoard()
        let doomed = StandardBoard.firstLaneCards[0]

        app.selectCard(doomed)
        app.clickMenuItem("Delete", in: "File")
        app.awaitLane(StandardBoard.toDo, cards: 2, "the deleted card should have left the lane")

        app.clickMenuItem("Show Trash", in: "View")
        app.awaitTrash(cards: 2)

        // The restore: cut in the trash, paste into a live lane.
        app.selectCard(doomed)
        app.clickMenuItem("Cut", in: "Edit")
        app.selectCard(StandardBoard.doneCard)
        app.clickMenuItem("Paste", in: "Edit")

        app.awaitLane(StandardBoard.done, cards: 2, "the restored card should have landed in Done")
        app.awaitTrash(cards: 1)
    }

    /// **File ▸ Empty Trash… (⇧⌘⌫)** — "the alert always appears" (03-board-ui.md § Trash), naming
    /// the true count, and confirming it purges the container.
    ///
    /// The trash has to be *shown* for the row to be enabled at all ("hidden, it is invisible to
    /// every gesture"), which is the first click here and is itself part of the claim.
    @MainActor
    func testEmptyTrash() throws {
        let app = XCUIApplication.launchedWithFixtureBoard()

        app.clickMenuItem("Show Trash", in: "View")
        app.awaitTrash(cards: 1)

        app.clickMenuItem("Empty Trash…", in: "File")

        // The prompt is `TrashModel.emptyTrashPrompt`: the count in the title, Delete and Cancel as
        // the two answers. It is asserted rather than merely dismissed, because "the alert always
        // appears" is the design rule under test.
        //
        // Queried app-wide rather than through `dialogs`/`sheets`, deliberately: which of those a
        // SwiftUI `.alert` surfaces as on macOS is the framework's choice and not a contract, while
        // the button titles and the sentence are `TrashConfirmations`' and `TrashModel`'s.
        let confirm = app.buttons["Delete"]
        XCTAssertTrue(
            confirm.waitForExistence(timeout: XCUIApplication.uiTimeout),
            "Empty Trash… did not raise its confirmation"
        )
        XCTAssertTrue(
            app.element(labelContaining: "Permanently delete \(Phrase.cards(1))").exists,
            "the confirmation did not name the trash's count"
        )
        XCTAssertTrue(app.buttons["Cancel"].exists, "the confirmation had no Cancel")

        confirm.click()

        app.awaitTrash(cards: 0)
    }

    // MARK: - Templates

    /// **File ▸ New Board… (⌥⌘N) as far as this suite can drive it** — the chooser opens, the bundled
    /// templates are listed and selectable, and Choose is live.
    ///
    /// ### Why it stops there
    ///
    /// Choose runs an `NSSavePanel`, and in a sandboxed app that panel is **Powerbox** — a separate,
    /// system-owned process (`com.apple.appkit.xpc.openAndSavePanelService`). Driving it means
    /// driving another application's UI from inside this one's test, on a surface Apple changes
    /// between releases and which has no accessibility contract of its own. A test that did it would
    /// fail for reasons that have nothing to do with this app, which is worse than not having the
    /// test: a flaky gate teaches people to re-run rather than to read.
    ///
    /// So the automated half ends at the panel, and **instantiating a template is a manual step** —
    /// written down in `KanbanUITests/EndToEndVerification.md` ▸ Manual-only flows rather than faked
    /// here. Everything behind the panel is unit-tested (`TemplateEngineTests` covers the copy, the
    /// `template:` key, the collision ladder and the cancellation).
    @MainActor
    func testTemplateChooserUpToTheSavePanel() throws {
        let app = XCUIApplication.launchedWithFixtureBoard()

        app.clickMenuItem("New Board…", in: "File")
        let chooser = app.windows["New Board"]
        XCTAssertTrue(
            chooser.waitForExistence(timeout: XCUIApplication.uiTimeout),
            "the template chooser did not appear"
        )

        // "Basic" is the bundled tier's lowest `template.order`, so it is the row the chooser opens
        // on — and a tile is a button to the accessibility tree, labeled by its name
        // (10-accessibility.md ▸ Template chooser).
        let basic = app.element(labeled: "Basic")
        XCTAssertTrue(basic.waitForExistence(timeout: XCUIApplication.uiTimeout), "the Basic template is not listed")
        basic.click()

        let choose = chooser.buttons["Choose"]
        XCTAssertTrue(choose.exists, "the chooser has no Choose button")
        XCTAssertTrue(choose.isEnabled, "Choose is disabled on a loadable template")

        // Cancel rather than Choose: pressing Choose hands the flow to Powerbox (see above). The
        // chooser closing is the last thing this test can honestly assert.
        chooser.buttons["Cancel"].click()
        XCTAssertTrue(
            chooser.waitForNonExistence(timeout: XCUIApplication.uiTimeout),
            "Cancel did not dismiss the template chooser"
        )
    }
}
