import AppKit
import Testing
@testable import Kanban

/// **`ColorSwatchPicker`'s pure pieces** (`ColorSwatchPicker.swift`), the 2026-08-10 rework that
/// retired `ColorComboView`'s `NSMenu` dropdown and its pure model — `ColorComboItem`, `ColorComboMatch`,
/// `ColorComboMenu`, `ColorComboModel.match`/`.menu`/`.displayName`/`.normalizedHex` — wholesale. This
/// file used to hold that model's tests; nothing in it survives the dropdown's removal, so it now
/// tests what replaced it: which palette a role offers, and the popover's own grid geometry.

// MARK: - Role

@Suite("ColorSwatchRole ▸ which palette")
struct ColorSwatchRoleTests {

    @Test("Background lists the backgrounds table, foreground the foregrounds table")
    func rolesListTheirOwnTable() {
        #expect(ColorSwatchRole.background.palette.map(\.name) == Palette.backgrounds.map(\.name))
        #expect(ColorSwatchRole.foreground.palette.map(\.name) == Palette.foregrounds.map(\.name))
    }
}

// MARK: - Popover geometry

/// **`SymbolPickerLayout`'s pattern, restated for a plain colour grid.** The layout is pure and
/// font-derived (10-accessibility.md's full-relative-scaling rule), so every claim below is
/// assertable without a popover on screen.
@Suite("ColorSwatchPopoverLayout ▸ geometry")
struct ColorSwatchPopoverLayoutTests {

    /// How wide `columns` wells and the gaps between them actually draw, at a given text size —
    /// `StyleEditorLayoutTests.gridWidth`'s own helper, restated for this layout's fixed seven.
    private func gridWidth(bodyPointSize: CGFloat) -> CGFloat {
        let columns = ColorSwatchPopoverLayout.columns
        let layout = ColorSwatchPopoverLayout.metrics(bodyPointSize: bodyPointSize)
        return CGFloat(columns) * layout.wellSide + CGFloat(columns - 1) * layout.wellSpacing
    }

    @Test("Seven columns — None + sixteen wells fall as 7 + 7 + 3, StyleEditorLayout.popover's own settled fit")
    func columnsAreSeven() {
        #expect(ColorSwatchPopoverLayout.columns == 7)
        // None + Palette.backgrounds (16) is 17 wells; seven columns is the count that keeps the
        // last row from overflowing to four rows or wasting a nearly-empty one.
        let wellCount = 1 + Palette.backgrounds.count
        let rows = (wellCount + ColorSwatchPopoverLayout.columns - 1) / ColorSwatchPopoverLayout.columns
        #expect(rows == 3)
    }

    @Test("The grid's well side matches the symbol popover's own scale-up over the shared base")
    func wellSideMatchesTheSymbolPopoversScale() {
        for size in [11.0, 13.0, 17.0, 24.0] as [CGFloat] {
            let color = ColorSwatchPopoverLayout.metrics(bodyPointSize: size)
            let symbol = SymbolPickerLayout.metrics(bodyPointSize: size)
            // Both scale `StyleEditorLayout.wellSide` by the identical `gridScale` — the two
            // popovers introduced together (2026-08-10) read at one scale.
            #expect(color.wellSide == symbol.wellSide, "the two popovers' wells drifted apart at \(size)pt")
            #expect(ColorSwatchPopoverLayout.gridScale == SymbolPickerLayout.gridScale)
        }
    }

    @Test("The popover's frame grows with the text, and its wells keep fitting inside it")
    func popoverScalesAndFits() {
        var previousWidth: CGFloat = 0
        for size in [11.0, 13.0, 16.0, 18.0, 24.0, 36.0] as [CGFloat] {
            let layout = ColorSwatchPopoverLayout.metrics(bodyPointSize: size)
            #expect(layout.popoverWidth > previousWidth, "the popover must widen with the text at \(size)pt")
            previousWidth = layout.popoverWidth

            let content = layout.popoverWidth - 2 * layout.contentPadding
            #expect(gridWidth(bodyPointSize: size) <= content, "the grid overflows its popover at \(size)pt")
        }
    }

    @Test("The popover's settled geometry at the standard text size")
    func settledGeometryAtTheStandardBody() {
        let layout = ColorSwatchPopoverLayout.metrics(bodyPointSize: 13)
        // `StyleEditorLayout`'s own statics at 13pt: wellSpacing 6, sectionSpacing 14. Well side is
        // the style editor's own 20pt scaled by `gridScale`, computed the identical way the
        // implementation does rather than restated as a literal — `20 * 1.3` is not exactly
        // representable in binary floating point, and comparing two independently-rounded literals
        // is exactly the kind of thing that can drift a bit apart from the real computation.
        #expect(layout.wellSpacing == 6)
        #expect(layout.contentPadding == 14)
        let expectedSide = (StyleEditorLayout.wellSide(bodyPointSize: 13) * ColorSwatchPopoverLayout.gridScale).rounded()
        #expect(layout.wellSide == expectedSide)
        #expect(layout.wellSide == 26)
        #expect(layout.gridWidth == layout.wellSide * 7 + layout.wellSpacing * 6)
        #expect(layout.popoverWidth == layout.gridWidth + layout.contentPadding * 2)
    }
}
