import Foundation
import Testing
@testable import Kanban

/// **The board popover's git-note detection** (12-editions.md ▸ The free tier and `.git`, ruled
/// 2026-07-27): the free tier's whole git story is one line, shown only on a board that carries an
/// inert `.git`. The seam
/// that decides *whether* to show it — `BoardGitNote.hasGitDirectory(at:)` — is a pure, read-only
/// `FileManager` check, pinned here against real bytes on disk. Everything else about the note (its
/// wording, its placement in `BoardInfoView`) is SwiftUI rendering and deliberately untested.
struct BoardInfoPopoverTests {

    @Test("A board with a .git at its root reports true")
    func trueForABoardWithGit() 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))

        #expect(BoardGitNote.hasGitDirectory(at: fixture.root))
    }

    @Test("An ordinary board with no .git reports false")
    func falseForAnOrdinaryBoard() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }

        try fixture.item("", Item.board)

        #expect(!BoardGitNote.hasGitDirectory(at: fixture.root))
    }
}

/// **The popover's git slot, posture by posture** (03-board-ui.md ▸ Board popover; 06-history-undo.md
/// ▸ Rules; 12-editions.md ▸ The free tier and `.git`).
///
/// `BoardGitSection.resolve` is the whole decision, pulled out as a pure function of the tier and the
/// board's mode precisely so the matrix is assertable — the views it selects are SwiftUI and stay
/// untested, exactly as the note's wording and placement do above.
@Suite("Board popover ▸ the git section's posture")
struct BoardGitSectionTests {

    @Test("The free tier: absent on an ordinary board, a Pro pointer on a board carrying an inert .git")
    func theFreeTierIsContextual() {
        #expect(BoardGitSection.resolve(tier: .free, mode: .none, hasGitDirectory: false) == .absent)
        #expect(BoardGitSection.resolve(tier: .free, mode: .none, hasGitDirectory: true) == .proPointer)
    }

    @Test("Pro: mode none has no repository to describe, git mode shows the branch")
    func proFollowsTheMode() {
        #expect(BoardGitSection.resolve(tier: .pro, mode: .none, hasGitDirectory: false) == .noRepository)
        #expect(BoardGitSection.resolve(tier: .pro, mode: .git, hasGitDirectory: true) == .branch)
    }

    @Test("A repo-nested board explains itself — setup is absent, not disabled")
    func repoNestedExplainsRatherThanDisables() {
        let section = BoardGitSection.resolve(tier: .pro, mode: .repoNested, hasGitDirectory: false)

        // The design is insistent here: "not a hidden 'add git' but a short explanation … the option
        // is absent because it *can't* apply, and the UI should teach that rather than look broken"
        // (06 ▸ Rules). A posture that rendered a disabled setup control would satisfy neither half —
        // and since the 2026-07-31 split the same sentence is what keeps the settings sheet itself
        // out of reach on such a board (`BoardSettingsAvailabilityTests`).
        #expect(section == .repoNested)
        #expect(section != .noRepository)
    }

    @Test("An unverifiable board gets its own posture — structurally like nested, never the same case")
    func unverifiableIsItsOwnPostureNotRepoNested() {
        // "Denial is not absence" (06 ▸ Rules ▸ Detection, ruled 2026-07-31): a denied ancestor check
        // is not a found repository, so the two must resolve to different cases even though both are
        // action-less, prose-only sections.
        let section = BoardGitSection.resolve(tier: .pro, mode: .unverifiable, hasGitDirectory: false)

        #expect(section == .unverifiable)
        #expect(section != .repoNested, "a denial is not a nesting")
        #expect(section != .noRepository)
    }

    /// **A board whose repository will not open keeps the git section it has** (06 ▸ Rules, the
    /// corrupt-`.git` loud failure, ruled 2026-07-31) — the *branch* section, held and explaining
    /// itself, never the mode-none posture that would imply a board with no history to have.
    ///
    /// The section case is deliberately blind to readability: what changes on such a board is what
    /// the branch surface inside it says (`BoardGitBranchSurface.resolve`, whose broken presentation
    /// and own sentence `BoardGitBranchSurfaceTests` pins), not which section the popover shows. The
    /// posture matrix stays a function of the tier and the mode alone.
    @Test("An unreadable repository is still the branch section — never the no-repository posture")
    func anUnreadableRepositoryKeepsTheBranchSection() {
        let section = BoardGitSection.resolve(tier: .pro, mode: .git, hasGitDirectory: true)

        #expect(section == .branch)
        #expect(section != .noRepository, "the board has a repository; it is unreadable, not absent")
    }

    @Test("Every posture is reachable, and none of them is two postures")
    func theMatrixIsTotal() {
        let resolved = Set(
            Tier.allCases.flatMap { tier in
                BoardGitMode.allCases.flatMap { mode in
                    [true, false].map { BoardGitSection.resolve(tier: tier, mode: mode, hasGitDirectory: $0) }
                }
            }
        )

        #expect(resolved == Set(BoardGitSection.allCases))
    }
}

/// **The window-title widget's two strings** (03-board-ui.md ▸ Board popover, the card that widened
/// the widget from a chevron to the whole board-name area): `BoardInfoTitlebarSummary` is the pure
/// function this pins, exactly as `BoardGitSectionTests` above pins the section it shares its
/// tier/mode inputs with. No disk I/O — the title half of this seam takes a raw `URL`, not a
/// `BoardStore`, so a plain `/tmp/...` path is enough.
@Suite("Board popover ▸ the widget's strings")
struct BoardInfoTitlebarSummaryTests {

    private let root = URL(fileURLWithPath: "/tmp/My Board.board")

    @Test("The title is the on-disk title, falling back to the folder name sans extension")
    func titleFallsBackToTheFolderName() {
        let named = BoardInfoTitlebarSummary(
            snapshotTitle: "Sprint 12", rootURL: root, tier: .free, mode: .none, branch: nil
        )
        #expect(named.title == "Sprint 12")

        let untitled = BoardInfoTitlebarSummary(
            snapshotTitle: nil, rootURL: root, tier: .free, mode: .none, branch: nil
        )
        #expect(untitled.title == "My Board")

        let emptyTitled = BoardInfoTitlebarSummary(
            snapshotTitle: "", rootURL: root, tier: .free, mode: .none, branch: nil
        )
        #expect(
            emptyTitled.title == "My Board",
            "an empty title reads the same as no title at all — the window-title rule this seam restates"
        )
    }

    @Test("The branch shows under Pro, in git mode, once it has been read")
    func branchShowsForProGitMode() {
        let summary = BoardInfoTitlebarSummary(
            snapshotTitle: nil, rootURL: root, tier: .pro, mode: .git, branch: "main"
        )
        #expect(summary.branch == "main")
    }

    @Test("The free tier never shows a branch, whatever the mode or the git state hands it")
    func theFreeTierNeverShowsABranch() {
        for mode in BoardGitMode.allCases {
            let summary = BoardInfoTitlebarSummary(
                snapshotTitle: nil, rootURL: root, tier: .free, mode: mode, branch: "main"
            )
            #expect(summary.branch == nil, "detection never runs under the free tier, so a stray branch value must never surface")
        }
    }

    @Test("An inert .git under Pro — mode none, repo-nested, or unverifiable — never shows a branch")
    func inertGitNeverShowsABranch() {
        #expect(
            BoardInfoTitlebarSummary(snapshotTitle: nil, rootURL: root, tier: .pro, mode: .none, branch: "main").branch == nil
        )
        #expect(
            BoardInfoTitlebarSummary(snapshotTitle: nil, rootURL: root, tier: .pro, mode: .repoNested, branch: "main").branch
                == nil
        )
        #expect(
            BoardInfoTitlebarSummary(snapshotTitle: nil, rootURL: root, tier: .pro, mode: .unverifiable, branch: "main").branch
                == nil
        )
    }

    @Test("A git-mode board whose branch has not been read yet shows none, honestly")
    func unreadBranchShowsNone() {
        let summary = BoardInfoTitlebarSummary(
            snapshotTitle: nil, rootURL: root, tier: .pro, mode: .git, branch: nil
        )
        #expect(summary.branch == nil)
    }
}
