import Foundation
import Testing
@testable import Kanban

/// **Nearest-`.git`-wins, freshly at every open** (06-history-undo.md ▸ Rules ▸ Detection) — the one
/// question every git surface starts from, pinned against real directories rather than against a
/// mocked filesystem, because what it is *about* is what is on disk.
///
/// The rule has four claims and this file is one test per claim: root wins, an ancestor is nested,
/// neither is `none`, and the answer is re-derived rather than remembered — "a board can therefore
/// change mode between opens (e.g. the user ran `git init` in a terminal) — the app just reflects
/// what it finds." `BoardGitEntryProbeTests` and `BoardGitModeDenialTests` below pin the
/// 2026-08-06 axis on top of it: "Denial is not absence" — a check the sandbox refuses must read as
/// `.unverifiable`, never as `.none`.

// MARK: - Fixtures

/// A `.git` **directory** with a plausible ref inside — what `git init` leaves.
private func makeGitDirectory(at parent: URL) throws {
    let gitDirectory = parent.appendingPathComponent(".git", isDirectory: true)
    try FileManager.default.createDirectory(at: gitDirectory, withIntermediateDirectories: true)
    try Data("ref: refs/heads/main\n".utf8).write(to: gitDirectory.appendingPathComponent("HEAD"))
}

/// A `.git` **file** — what a linked worktree or a submodule leaves. Still a repository, and the
/// reason detection asks `fileExists` rather than `isDirectory`.
private func makeGitPointerFile(at parent: URL, to target: String) throws {
    try Data("gitdir: \(target)\n".utf8).write(to: parent.appendingPathComponent(".git"))
}

/// A board folder inside the fixture, so an *ancestor* can carry the repository.
private func makeSubfolder(_ fixture: WriterFixture, named name: String) throws -> URL {
    let url = fixture.root.appendingPathComponent(name, isDirectory: true)
    try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
    return url
}

// MARK: - Detection

@Suite("Board git mode ▸ detection")
struct BoardGitModeTests {

    @Test("A `.git` at the board root is git mode")
    func rootRepositoryIsGitMode() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("", Item.board)
        try makeGitDirectory(at: fixture.root)

        #expect(BoardGitMode.detect(boardRoot: fixture.root) == .git)
    }

    @Test("A `.git` *file* is a repository too — a worktree is not mode none")
    func aWorktreePointerIsGitMode() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("", Item.board)
        try makeGitPointerFile(at: fixture.root, to: "/somewhere/else/.git/worktrees/board")

        #expect(BoardGitMode.detect(boardRoot: fixture.root) == .git)
    }

    @Test("No `.git` at the root and none above it is mode none")
    func aPlainFolderIsModeNone() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("", Item.board)

        #expect(BoardGitMode.detect(boardRoot: fixture.root) == .none)
    }

    @Test("A `.git` at an ancestor and none at the root is repo-nested")
    func anEnclosingRepositoryIsRepoNested() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try makeGitDirectory(at: fixture.root)
        let board = try makeSubfolder(fixture, named: "project/docs/board")

        #expect(BoardGitMode.detect(boardRoot: board) == .repoNested)
        #expect(BoardGitMode.enclosingRepositoryRoot(above: board)?.standardizedFileURL
            == fixture.root.standardizedFileURL)
    }

    @Test("Nearest wins: a board with its own `.git` inside a repository is git mode, not nested")
    func theNearestRepositoryWins() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try makeGitDirectory(at: fixture.root)
        let board = try makeSubfolder(fixture, named: "board")
        try makeGitDirectory(at: board)

        #expect(BoardGitMode.detect(boardRoot: board) == .git)
    }

    @Test("The walk terminates above a board at the filesystem root, finding nothing")
    func theAncestorWalkTerminates() {
        // The one hazard this walk has ever had: NSURL-bridged URLs whose
        // `deletingLastPathComponent` grows "/.." forever instead of settling at "/". The temp
        // directory has no repository above it, so the honest answer is `nil` — reached, not hung.
        #expect(BoardGitMode.enclosingRepositoryRoot(above: URL(fileURLWithPath: "/")) == nil)
    }

    // MARK: Freshness

    @Test("Detection is re-derived at every open: a board that gains a `.git` opens in git mode next time")
    func modeChangesBetweenOpens() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("", Item.board)

        #expect(BoardGitMode.detect(boardRoot: fixture.root) == .none, "the first open")

        // What a user does in a terminal under a closed board.
        try makeGitDirectory(at: fixture.root)

        #expect(BoardGitMode.detect(boardRoot: fixture.root) == .git, "the next open reflects what it finds")
    }

    @Test("And a board that loses its `.git` opens back in mode none")
    func modeChangesBackBetweenOpens() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("", Item.board)
        try makeGitDirectory(at: fixture.root)
        #expect(BoardGitMode.detect(boardRoot: fixture.root) == .git)

        try FileManager.default.removeItem(at: fixture.root.appendingPathComponent(".git"))

        #expect(BoardGitMode.detect(boardRoot: fixture.root) == .none)
    }
}

// MARK: - Entry probe classification

/// **The errno-aware probe underneath detection** (06-history-undo.md ▸ Rules ▸ Detection, "Denial
/// is not absence", ruled 2026-07-31) — pinned directly, one test per classification, before the
/// walk that builds on it is asked to prove anything.
///
/// The `denied` cases chmod a real directory to `0o000` — tests run unprivileged, so `EACCES` is
/// genuinely reachable this way — and restore it with an explicit `defer` declared *after* the
/// fixture's own teardown defer, so it runs first (Swift's LIFO defer order):
/// `BoardDuplicatorTests.aFailedWalkRemovesThePartialSibling` is the precedent this mirrors, so a
/// failed assertion can never leave an unremovable temp directory behind.
@Suite("Board git mode ▸ entry probe")
struct BoardGitEntryProbeTests {

    @Test("A `.git` directory probes as exists")
    func probesExists() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try makeGitDirectory(at: fixture.root)

        #expect(BoardGitMode.probeGitEntry(at: fixture.root) == .exists)
        #expect(BoardGitMode.hasGitEntry(at: fixture.root), "the boolean convenience agrees")
    }

    @Test("A plain folder with no `.git` probes as absent")
    func probesAbsent() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("", Item.board)

        #expect(BoardGitMode.probeGitEntry(at: fixture.root) == .absent)
        #expect(!BoardGitMode.hasGitEntry(at: fixture.root))
    }

    @Test("A folder somewhere the sandbox denies traversal probes as denied, not absent")
    func probesDenied() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let outer = fixture.root.appendingPathComponent("outer", isDirectory: true)
        let inner = outer.appendingPathComponent("inner", isDirectory: true)
        try FileManager.default.createDirectory(at: inner, withIntermediateDirectories: true)

        // Chmod the *parent*, not the probed folder itself: resolving `inner/.git` needs search
        // permission on `outer`, which a plain unix permission bit can deny for the test's own
        // unprivileged user exactly as the sandbox denies an ungranted ancestor.
        try FileManager.default.setAttributes([.posixPermissions: 0o000], ofItemAtPath: outer.path)
        defer { try? FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: outer.path) }

        #expect(BoardGitMode.probeGitEntry(at: inner) == .denied)
        #expect(!BoardGitMode.hasGitEntry(at: inner), "the boolean convenience collapses denied to false, like absent")
    }
}

// MARK: - Denial-aware detection

/// **The walk semantics denial adds** (06 ▸ Rules ▸ Detection): a denied ancestor never ends the
/// walk early, because a farther ancestor's `.git` still makes repo-nested certain; only a walk that
/// finds nothing at all *and* saw a denial along the way reads `.unverifiable`.
@Suite("Board git mode ▸ denial-aware detection")
struct BoardGitModeDenialTests {

    @Test("A denied board-root probe is unverifiable outright — the walk never runs")
    func deniedRootProbeIsUnverifiable() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let boardRoot = fixture.root.appendingPathComponent("board", isDirectory: true)
        try FileManager.default.createDirectory(at: boardRoot, withIntermediateDirectories: true)

        try FileManager.default.setAttributes([.posixPermissions: 0o000], ofItemAtPath: boardRoot.path)
        defer { try? FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: boardRoot.path) }

        #expect(BoardGitMode.detect(boardRoot: boardRoot) == .unverifiable)
    }

    @Test("A denied ancestor with nothing found anywhere else reads unverifiable")
    func deniedAncestorWithNothingFoundIsUnverifiable() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        let blocked = fixture.root.appendingPathComponent("blocked", isDirectory: true)
        let boardRoot = blocked.appendingPathComponent("board", isDirectory: true)
        try FileManager.default.createDirectory(at: boardRoot, withIntermediateDirectories: true)

        try FileManager.default.setAttributes([.posixPermissions: 0o000], ofItemAtPath: blocked.path)
        defer { try? FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: blocked.path) }

        let walk = BoardGitMode.ancestorWalk(above: boardRoot)
        #expect(walk.root == nil)
        #expect(walk.sawDenial)
        #expect(BoardGitMode.detect(boardRoot: boardRoot) == .unverifiable)
    }

    @Test("A denied nearer ancestor never hides a `.git` on a farther one — repo-nested is certain")
    func deniedAncestorWithARepositoryFartherUpIsRepoNested() throws {
        // **A note on what chmod can and cannot simulate**: the sandbox denies a *specific path*
        // independently of the filesystem's own permission bits — an ancestor above the board's
        // grant can be denied while the board root itself, inside the grant, stays fully readable.
        // POSIX `chmod`, in contrast, cascades: removing search permission from a real ancestor
        // directory denies resolving *everything* beneath it, board root included, which is a
        // strictly stronger (and still individually honest) denial than the sandbox's. So this test
        // proves the walk's own claim directly — `ancestorWalk(above:)` never touches `boardRoot`
        // itself, only the candidates above it, and is unaffected by that cascade.
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try makeGitDirectory(at: fixture.root)
        let blocked = fixture.root.appendingPathComponent("blocked", isDirectory: true)
        let boardRoot = blocked.appendingPathComponent("board", isDirectory: true)
        try FileManager.default.createDirectory(at: boardRoot, withIntermediateDirectories: true)

        try FileManager.default.setAttributes([.posixPermissions: 0o000], ofItemAtPath: blocked.path)
        defer { try? FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: blocked.path) }

        // "A farther ancestor showing `.git` makes repo-nested certain regardless of the denied
        // nearer one — nearest-wins only affects which root you'd name, not whether one exists."
        let walk = BoardGitMode.ancestorWalk(above: boardRoot)
        #expect(walk.root?.standardizedFileURL == fixture.root.standardizedFileURL)
        #expect(walk.sawDenial, "the denial is still recorded, even though it didn't decide the outcome")

        // `detect(boardRoot:)` itself reads `.unverifiable` here — not `.repoNested` — but for the
        // cascade reason above, not because the walk's certainty claim is false: `blocked` sits
        // between the filesystem root and `boardRoot`, so chmoding it also denies **`boardRoot`'s
        // own** `.git` probe, and `detect` answers that denial before the ancestor walk ever runs
        // (06 ▸ Rules: "probe the board root's `.git` first … denied → `.unverifiable`"). A real
        // sandboxed board, whose own root sits inside the grant, would not hit this path — its own
        // probe would succeed and the walk above is what would then run and find `.repoNested`.
        #expect(BoardGitMode.detect(boardRoot: boardRoot) == .unverifiable)
    }

    @Test("All-clean paths are unaffected: none, git, and repo-nested still read as before")
    func cleanPathsAreUnaffected() throws {
        let plain = try WriterFixture()
        defer { plain.tearDown() }
        try plain.item("", Item.board)
        #expect(BoardGitMode.detect(boardRoot: plain.root) == .none)

        let gitBoard = try WriterFixture()
        defer { gitBoard.tearDown() }
        try makeGitDirectory(at: gitBoard.root)
        #expect(BoardGitMode.detect(boardRoot: gitBoard.root) == .git)

        let nested = try WriterFixture()
        defer { nested.tearDown() }
        try makeGitDirectory(at: nested.root)
        let board = try makeSubfolder(nested, named: "project/docs/board")
        #expect(BoardGitMode.detect(boardRoot: board) == .repoNested)
    }
}
