import Foundation
import Testing
@testable import Kanban

/// **The board settings sheet's two pure seams** (03-board-ui.md ▸ Board settings sheet, ruled
/// 2026-07-31 — the popover/sheet split; 04-interactions.md ▸ The map's configuration carve-out):
/// what the sheet holds for a board, and therefore whether the sheet exists for that board at all.
///
/// They are pinned here for `BoardGitSection.resolve`'s reason one surface over: the *inventory* is
/// the decision worth asserting and the SwiftUI that renders it is not. Every case below is a plain
/// value — no board on disk, no window, no session.
@Suite("Board settings sheet ▸ the sections")
struct BoardSettingsSectionTests {

    @Test("Pro, mode none: the sheet is add-git and nothing else")
    func modeNoneHoldsAddGit() {
        // "add-git (mode none; opt-in init — 06)" — 03's own first entry for this surface, and the
        // control that moved here out of the popover with the split.
        #expect(BoardSettingsSection.resolve(tier: .pro, mode: .none) == [.git])
    }

    @Test("Pro, git mode: branch creation and the commit identity, in that order")
    func gitModeHoldsCreationAndIdentity() {
        // "branch creation (switching stays in the popover…)" and "commit identity name/email (06 —
        // the visibility-scoped 2 s config re-read rides with the fields)".
        #expect(BoardSettingsSection.resolve(tier: .pro, mode: .git) == [.branch, .commitIdentity])
    }

    @Test("Every section is reachable from some posture, and no posture invents one")
    func theInventoryIsTotal() {
        let offered = Set(
            Tier.allCases.flatMap { tier in
                BoardGitMode.allCases.flatMap { BoardSettingsSection.resolve(tier: tier, mode: $0) }
            }
        )

        #expect(offered == Set(BoardSettingsSection.allCases))
    }

    @Test("Pro, unverifiable: the sheet has nothing to show — structurally like repo-nested")
    func unverifiableHoldsNothing() {
        // "Denial is not absence" (06 ▸ Rules ▸ Detection, ruled 2026-07-31): a denied ancestor check
        // can never be told apart from a repository actually being there, so add-git stays as
        // unreachable here as it is on a genuinely nested board.
        #expect(BoardSettingsSection.resolve(tier: .pro, mode: .unverifiable) == [])
    }

    @Test("Pro, git mode with an unreadable repository: nothing setup-shaped applies either")
    func anUnreadableRepositoryHoldsNothing() {
        // **The corrupt-`.git` loud failure** (06 ▸ Rules, ruled 2026-07-31): both sections here are
        // *writes* to a repository — a branch created in it, an identity written into its config —
        // and there is no repository the app can open to write either into. Repo-nested's emptiness,
        // reached one step further along.
        #expect(BoardSettingsSection.resolve(tier: .pro, mode: .git, isRepositoryUnreadable: true) == [])

        // …and the mode is still `git` throughout: this is emptiness *within* git mode, never the
        // fall to mode none that would let add-git be offered against an existing `.git`.
        #expect(BoardSettingsSection.resolve(tier: .pro, mode: .git) == [.branch, .commitIdentity])
    }

    @Test("The sections carry the headers VoiceOver navigates by")
    func headersAreNamed() {
        // 10-accessibility.md ▸ Board settings sheet: "titled and sectioned with headers VoiceOver
        // can navigate by". The strings are the surface's spoken structure, so they are stated once
        // and pinned once.
        #expect(BoardSettingsSection.git.title == "Git")
        #expect(BoardSettingsSection.branch.title == "Branch")
        #expect(BoardSettingsSection.commitIdentity.title == "Commit Identity")
    }
}

/// **Where the sheet can be opened from, tier by mode** — the answer both doors validate on: Board ▸
/// Board Settings…'s `disabled` state, and whether the popover's git section renders its Board
/// Settings… row at all.
@Suite("Board settings sheet ▸ availability")
struct BoardSettingsAvailabilityTests {

    @Test("The whole matrix: Pro on a none-or-git board, and nowhere else")
    func theMatrix() {
        // Pro, and a board whose mode leaves something to set up.
        #expect(BoardSettingsAvailability.resolve(tier: .pro, mode: .none))
        #expect(BoardSettingsAvailability.resolve(tier: .pro, mode: .git))

        // **Pro, repo-nested**: "nothing setup-shaped can apply" — no add-git (06's prose, not a
        // disabled button), no branch of ours to create, no repo-local config of ours to write. The
        // popover's explanation stands and no door opens.
        #expect(!BoardSettingsAvailability.resolve(tier: .pro, mode: .repoNested))

        // **Pro, unverifiable**: the same unreachability, for the denial-not-absence reason — a
        // denied ancestor check is never distinguishable from a repository actually being there.
        #expect(!BoardSettingsAvailability.resolve(tier: .pro, mode: .unverifiable))

        // **Pro, git mode over a repository that will not open**: no door either, so neither the
        // popover's Board Settings… row nor the menu command offers a surface with nothing on it
        // (06 ▸ Rules, the corrupt-`.git` loud failure).
        #expect(!BoardSettingsAvailability.resolve(tier: .pro, mode: .git, isRepositoryUnreadable: true))

        // **The free tier**: no setup exists there at all (12-editions.md ▸ The free tier and
        // `.git`), whatever mode a stray value claims — detection never runs off Pro, so the mode is
        // swept for completeness rather than because it can vary.
        for mode in BoardGitMode.allCases {
            #expect(
                !BoardSettingsAvailability.resolve(tier: .free, mode: mode),
                "the free tier has no board settings sheet in any mode"
            )
        }
    }

    @Test("Reachable means exactly 'has something to show'")
    func reachabilityIsTheInventory() {
        // The derivation, not a coincidence: a surface whose whole job is hosting setup controls has
        // no honest empty state, so the two answers are one answer. pro-m2's sections join the
        // inventory and this identity keeps holding.
        for tier in Tier.allCases {
            for mode in BoardGitMode.allCases {
                #expect(
                    BoardSettingsAvailability.resolve(tier: tier, mode: mode)
                        == !BoardSettingsSection.resolve(tier: tier, mode: mode).isEmpty
                )
            }
        }
    }
}

/// **The window's sheet flag** (`BoardSettingsPresentation`) — `BoardInfoPresentation`'s sibling,
/// with the one thing the popover flag does not carry: the session posture both doors validate on.
@MainActor
@Suite("Board settings sheet ▸ the window's presentation")
struct BoardSettingsPresentationTests {

    @Test("It starts closed, unadopted, and unreachable")
    func startsClosed() {
        let presentation = BoardSettingsPresentation()

        #expect(presentation.isPresented == false)
        #expect(presentation.tier == .free)
        #expect(presentation.git == nil)
        // The free tier's posture is the safe default for a window whose session has not been adopted
        // yet: an unreachable sheet, rather than a sheet with no sections in it.
        #expect(presentation.isReachable == false)
        #expect(presentation.sections.isEmpty)
    }

    @Test("An unreachable board's sheet refuses to present")
    func presentRefusesWhereUnreachable() {
        let presentation = BoardSettingsPresentation()

        presentation.present()
        #expect(presentation.isPresented == false, "the free tier has no sheet to open")
    }

    @Test("Two windows hold their own flags")
    func perWindow() {
        // `BoardInfoPresentation`'s rule, restated for the sheet: "two board windows each hold their
        // own and can never toggle each other's".
        let first = BoardSettingsPresentation()
        let second = BoardSettingsPresentation()

        first.isPresented = true
        #expect(second.isPresented == false)
    }

    @Test("Adopting a Pro git session makes the sheet reachable, and dismissal is idempotent")
    func adoptingASession() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("", Item.board)

        // Mode `none` — the add-git posture, which is the sheet's whole job on a board with no
        // repository yet.
        let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
        #expect(git.mode == .none)

        let presentation = BoardSettingsPresentation()
        presentation.adopt(tier: .pro, git: git)

        #expect(presentation.isReachable)
        #expect(presentation.sections == [.git])

        presentation.present()
        #expect(presentation.isPresented)

        presentation.dismiss()
        presentation.dismiss()
        #expect(presentation.isPresented == false)
    }

    @Test("The free tier's session leaves both doors shut, .git on the folder or not")
    func aFreeTierSessionIsUnreachable() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("", Item.board)
        try fixture.file(".git/HEAD", Data("ref: refs/heads/main\n".utf8))

        // `compose` is the tier gate: the free tier gets no git state at all, so there is nothing for
        // a settings sheet to be about even on a board carrying an inert `.git` — the popover's
        // one-line Pro pointer is that board's whole story (12-editions.md).
        #expect(HistoryStore.compose(boardRoot: fixture.root, tier: .free) == nil)

        let presentation = BoardSettingsPresentation()
        presentation.adopt(tier: .free, git: nil)

        #expect(presentation.isReachable == false)
        presentation.present()
        #expect(presentation.isPresented == false)
    }
}
