import Testing
@testable import Kanban

/// `caretChordsYield(boardInfo:search:)` — 04-interactions.md ▸ Grammar's caret-chords
/// rule as one expression, and `BoardCommands.swift`'s single seam for it: Board ▸ Move Left/Move
/// Right ⌘←/⌘→ and the lane-width pair ⌥⌘←/⌥⌘→ disable via menu validation whenever *any* text
/// control has keyboard focus, because ⌘←/⌘→ are the standard line-start/end caret chords and an
/// enabled key equivalent fires before a field ever sees the key.
///
/// The function reads exactly two flags and nothing else, so every test here constructs
/// `BoardInfoPresentation` and `BoardSearchPresentation` directly rather than through a `BoardStore`
/// — a fixture that stood up a board would be exercising machinery this seam never touches.
///
/// **It read three until 2026-08-07.** The 2026-07-31 popover/sheet split gave the board settings
/// sheet its own flag here, because the branch-name and commit-identity fields had moved onto it; the
/// reversal retired that sheet and brought those fields back into the popover's Git tab, so the
/// popover's own open-at-all clause covers every configuration field again and the third disjunct
/// went with the surface it described. (The Git tab itself retired in turn, 2026-08-08 with the git
/// excision, `strategy/01-git-excision.md`; the two-flag shape this file tests did not change again.)
@MainActor
@Suite("caretChordsYield ▸ the caret-chords rule")
struct CaretChordTests {

    // MARK: Scopeless windows

    @Test("A card window publishes neither presentation, and the chords stay enabled")
    func scopelessWindowDoesNotYield() {
        // "Card-window fields need nothing: those windows never publish a boardStore, so both items
        // are already scopeless there" (caretChordsYield's doc comment) — nil/nil is that window's
        // steady state, not a corner case.
        #expect(caretChordsYield(boardInfo: nil, search: nil) == false)
    }

    // MARK: The board popover, alone

    @Test("The board popover open yields; closed, it does not")
    func popoverPresence() {
        let boardInfo = BoardInfoPresentation()
        #expect(caretChordsYield(boardInfo: boardInfo, search: nil) == false, "closed by default")

        boardInfo.isPresented = true
        #expect(caretChordsYield(boardInfo: boardInfo, search: nil) == true)

        boardInfo.isPresented = false
        #expect(caretChordsYield(boardInfo: boardInfo, search: nil) == false, "closing re-enables the chords")
    }

    // MARK: The search field, alone

    @Test("The search field focused yields; unfocused, it does not")
    func searchFocus() {
        let search = BoardSearchPresentation()
        #expect(caretChordsYield(boardInfo: nil, search: search) == false, "unfocused by default")

        search.isFocused = true
        #expect(caretChordsYield(boardInfo: nil, search: search) == true)

        search.isFocused = false
        #expect(caretChordsYield(boardInfo: nil, search: search) == false, "losing focus re-enables the chords")
    }

    // MARK: Both surfaces together

    @Test("Either surface active is enough to yield; both absent-or-inert is the only false")
    func eitherSurfaceIsSufficient() {
        let closedInfo = BoardInfoPresentation()
        let openInfo = BoardInfoPresentation()
        openInfo.isPresented = true
        let unfocusedSearch = BoardSearchPresentation()
        let focusedSearch = BoardSearchPresentation()
        focusedSearch.isFocused = true

        // One side published and inert, the other absent (the still-loading-window shape): false.
        #expect(caretChordsYield(boardInfo: closedInfo, search: nil) == false)
        #expect(caretChordsYield(boardInfo: nil, search: unfocusedSearch) == false)

        // One side published and active, the other absent: true.
        #expect(caretChordsYield(boardInfo: openInfo, search: nil) == true)
        #expect(caretChordsYield(boardInfo: nil, search: focusedSearch) == true)

        // Both published, both inert: false — the popover being open at all and the field holding
        // focus are each read independently, so neither's mere presence counts on its own.
        #expect(caretChordsYield(boardInfo: closedInfo, search: unfocusedSearch) == false)

        // Both published, one or both active: true. This is an `||`, not an `&&` — one live text
        // surface is enough to send the chords to it, whatever the other surface is doing.
        #expect(caretChordsYield(boardInfo: openInfo, search: unfocusedSearch) == true)
        #expect(caretChordsYield(boardInfo: closedInfo, search: focusedSearch) == true)
        #expect(caretChordsYield(boardInfo: openInfo, search: focusedSearch) == true)
    }

    // MARK: Inline editors are a different seam

    @Test("An inline title editor is not this seam's job — acceptsBoardMutations covers it")
    func inlineEditingIsAForeignConcern() {
        // Inline title editors are the caret-chords rule's *first* named surface (the doc comment's
        // list), but they are answered through `acceptsBoardMutations`, not here: `MoveLaneCommands`
        // and `LaneWidthCommands` each disable on `yieldsCaretChords || <the acceptsBoardMutations
        // check>`, so a rename or a new-card placeholder already closes the items through that other
        // half of the `||` before this function is ever asked. Pinning `caretChordsYield`'s own
        // independence from `isEditingInline` is what keeps that split honest.
        let transient = TransientBoardState()
        transient.beginRename(of: ItemID(rawValue: "11111111-1111-4111-8111-111111111111"), currentTitle: "Card")
        #expect(transient.isEditingInline == true, "the rename editor is open")

        // No board popover, no search focus — caretChordsYield answers false regardless of the open
        // editor above, because it never reads isEditingInline at all.
        #expect(caretChordsYield(boardInfo: nil, search: nil) == false)
    }
}
