import Foundation
import Testing
@testable import Kanban

/// **The Info tab's two seams** (03-board-ui.md § Board popover ▸ Info tab, settled 2026-08-07),
/// pinned the way the popover's other pure seams are: `BoardInfoMetrics` against a loaded fixture
/// board, `BoardDiskFootprint` against real bytes on disk. The rows' rendering — labels, the em-dash
/// placeholder, the Reveal button — is SwiftUI and deliberately untested.
@Suite("Board popover ▸ Info tab metrics")
struct BoardInfoTabTests {

    /// Two live lanes holding three cards (one carrying two attachments), a loose trashed card, and
    /// a trashed lane holding two more — the shape that exercises every counting rule at once.
    private func makeFixture() throws -> WriterFixture {
        let fixture = try WriterFixture()

        try fixture.item("", Item.board)
        try fixture.item(Ident.lane1, Item.rich(order: "1024", title: "Doing"))
        try fixture.item("\(Ident.lane1)/\(Ident.card1)", Item.rich(order: "1024", title: "One"))
        try fixture.item("\(Ident.lane1)/\(Ident.card2)", Item.rich(order: "2048", title: "Two"))
        try fixture.file("\(Ident.lane1)/\(Ident.card1)/attachments/sketch.png", Data([0xFF, 0xD8]))
        try fixture.file("\(Ident.lane1)/\(Ident.card1)/attachments/notes.txt", Data("notes".utf8))
        try fixture.item(Ident.lane2, Item.rich(order: "2048", title: "Done"))
        try fixture.item("\(Ident.lane2)/\(Ident.card3)", Item.rich(order: "1024", title: "Three"))

        try fixture.item(".trash/\(Ident.card4)", Item.rich(order: "1024", title: "Tossed"))
        try fixture.item(
            ".trash/\(Ident.lane3)",
            "---\nschema: 1\ntitle: Shipped\norder: 3072\nkind: lane\n---\n"
        )
        try fixture.item(
            ".trash/\(Ident.lane3)/aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
            Item.rich(order: "1024", title: "Held one")
        )
        try fixture.item(
            ".trash/\(Ident.lane3)/bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
            Item.rich(order: "2048", title: "Held two")
        )

        return fixture
    }

    @Test("Lanes and cards count the live board only — the welcome-count rule")
    func lanesAndCardsAreLiveOnly() throws {
        let fixture = try makeFixture()
        defer { fixture.tearDown() }

        let metrics = BoardInfoMetrics(snapshot: try BoardLoader.load(boardRoot: fixture.root).model)

        #expect(metrics.lanes == 2)
        #expect(metrics.cards == 3)
    }

    @Test("The trash tail counts freight — loose cards plus every trashed lane's held cards")
    func trashedCardsCountFreight() throws {
        let fixture = try makeFixture()
        defer { fixture.tearDown() }

        let metrics = BoardInfoMetrics(snapshot: try BoardLoader.load(boardRoot: fixture.root).model)

        // One loose trashed card, two held by the trashed lane — the purge-confirm counting rule.
        #expect(metrics.trashedCards == 3)
    }

    @Test("Attachments count the live cards' files")
    func attachmentsCountLiveCards() throws {
        let fixture = try makeFixture()
        defer { fixture.tearDown() }

        let metrics = BoardInfoMetrics(snapshot: try BoardLoader.load(boardRoot: fixture.root).model)

        #expect(metrics.attachments == 2)
    }

    @Test("An empty board is all zeros, not a crash or a lie")
    func emptyBoardIsZeros() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }
        try fixture.item("", Item.board)

        let metrics = BoardInfoMetrics(snapshot: try BoardLoader.load(boardRoot: fixture.root).model)

        #expect(metrics.lanes == 0)
        #expect(metrics.cards == 0)
        #expect(metrics.trashedCards == 0)
        #expect(metrics.attachments == 0)
    }
}

/// **The whole-folder walk** — the disk rows exist to agree with Finder's Get Info, so the walk
/// must count hidden entries (`.git`, `.trash/`) exactly as it counts the visible board.
@Suite("Board popover ▸ Info tab disk footprint")
struct BoardDiskFootprintTests {

    @Test("Files counts every regular file in the tree, hidden entries included")
    func filesCountsTheWholeFolder() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }

        try fixture.item("", Item.board)
        try fixture.item(Ident.lane1, Item.rich(order: "1024", title: "Doing"))
        try fixture.item("\(Ident.lane1)/\(Ident.card1)", Item.rich(order: "1024", title: "One"))
        try fixture.file("\(Ident.lane1)/\(Ident.card1)/attachments/sketch.png", Data([0xFF, 0xD8]))
        try fixture.file(".git/HEAD", Data("ref: refs/heads/main\n".utf8))
        try fixture.item(".trash/\(Ident.card2)", Item.rich(order: "1024", title: "Tossed"))

        let footprint = BoardDiskFootprint.measure(at: fixture.root)

        // Three index.md files live (board, lane, card), one attachment, one .git file, one trashed
        // card's index.md — six regular files; directories are containers, not files.
        #expect(footprint.files == 6)
    }

    @Test("Size on disk is at least the logical bytes — allocation rounds up, never down to zero")
    func bytesCoverTheLogicalSize() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }

        try fixture.item("", Item.board)
        let payload = Data(repeating: 0x2A, count: 4096)
        try fixture.file("stray.bin", payload)

        let footprint = BoardDiskFootprint.measure(at: fixture.root)

        let logical = Int64(payload.count + Data(Item.board.utf8).count)
        #expect(footprint.bytes >= logical)
    }

    @Test("Created and Modified come back from a real folder, and never out of order")
    func datesAreReadAndOrdered() throws {
        let fixture = try WriterFixture()
        defer { fixture.tearDown() }

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

        let footprint = BoardDiskFootprint.measure(at: fixture.root)

        let created = try #require(footprint.created)
        let modified = try #require(footprint.modified)
        #expect(modified >= created)
    }

    @Test("A vanished folder answers empty rather than trapping — the walk has no user to ask")
    func vanishedFolderAnswersEmpty() {
        let gone = URL(fileURLWithPath: "/tmp/lanework-info-tab-never-existed-\(UUID().uuidString)")

        let footprint = BoardDiskFootprint.measure(at: gone)

        #expect(footprint.files == 0)
        #expect(footprint.bytes == 0)
    }
}
