import SwiftUI
import Testing
@testable import Kanban

/// **`BoardThemeFilters`** and **`BoardThemeMode`** — the Theme tab's shared filter state and its
/// Solid color / Pattern picker: the default-tone rule, the pure filters → `FacetsRecipe` assembly,
/// and solid colour's seed-independence, pinned the way the popover's other pure seams are
/// (`BoardInfoMetrics`, `BoardDiskFootprint` in `BoardInfoTabTests`). Everything else about the
/// carousel — the strip's layout, the placeholder chip, the chevron paging, the apply gesture — is
/// SwiftUI and deliberately untested; `FacetsGeneratorTests` and `GeneratedBackgroundTests` already
/// cover the generator and the write paths this feeds.
@Suite("Board popover ▸ Theme tab filters")
struct BoardThemeFiltersTests {

    @Test("Dark system appearance opens on Tone Dark, light opens on Tone Light")
    func defaultToneFollowsTheSystem() {
        #expect(BoardThemeFilters.defaultTone(colorScheme: .dark) == .dark)
        #expect(BoardThemeFilters.defaultTone(colorScheme: .light) == .light)
    }

    @Test("The opening state is mono, medium, mid — only tone varies with the system")
    func initialStateIsTheReviewedDefaults() {
        let light = BoardThemeFilters.initial(colorScheme: .light)
        #expect(light.tone == .light)
        #expect(light.colors == .mono)
        #expect(light.mesh == .medium)
        #expect(light.saturation == .mid)

        let dark = BoardThemeFilters.initial(colorScheme: .dark)
        #expect(dark.tone == .dark)
        #expect(dark.colors == .mono)
        #expect(dark.mesh == .medium)
        #expect(dark.saturation == .mid)
    }

    @Test("A hue and a seed assemble the exact recipe the filters describe")
    func recipeAssemblesEveryAxis() {
        let filters = BoardThemeFilters(tone: .dark, colors: .trio, mesh: .fine, saturation: .rich)
        let recipe = filters.recipe(hue: .iris, seed: 0x5EED)

        #expect(recipe.hue == .iris)
        #expect(recipe.strategy == .trio)
        #expect(recipe.density == .fine)
        #expect(recipe.tone == .dark)
        #expect(recipe.saturation == .rich)
        #expect(recipe.seed == 0x5EED)
    }

    @Test("Two hues under the same filters and seed differ only in hue")
    func onlyHueChangesAcrossTheWheel() {
        let filters = BoardThemeFilters.initial(colorScheme: .light)
        let sky = filters.recipe(hue: .sky, seed: 42)
        let rose = filters.recipe(hue: .rose, seed: 42)

        #expect(sky.hue == .sky)
        #expect(rose.hue == .rose)
        #expect(sky.strategy == rose.strategy)
        #expect(sky.density == rose.density)
        #expect(sky.tone == rose.tone)
        #expect(sky.saturation == rose.saturation)
        #expect(sky.seed == rose.seed)
    }

    @Test("The same filters and seed recipe identically — the preview/apply agreement the carousel depends on")
    func sameInputsRecipeIdentically() {
        let filters = BoardThemeFilters(tone: .light, colors: .duo, mesh: .coarse, saturation: .soft)
        #expect(filters.recipe(hue: .forest, seed: 7) == filters.recipe(hue: .forest, seed: 7))
    }

    // MARK: - Mode

    @Test("Solid color and Pattern are the picker's two cases, labeled as the segmented control shows them")
    func modeCasesAreTheSegmentedLabels() {
        #expect(BoardThemeMode.allCases == [.solid, .pattern])
        #expect(BoardThemeMode.solid.rawValue == "Solid color")
        #expect(BoardThemeMode.pattern.rawValue == "Pattern")
        #expect(BoardThemeMode.solid.id == .solid)
        #expect(BoardThemeMode.pattern.id == .pattern)
    }

    // MARK: - Solid colour derivation

    /// The Solid color swatch's fill (`BoardThemeTabView.solidColor(_:)`): the filters' own
    /// tone/saturation level at a hue, un-jittered — the same thing a Pattern swatch's ground colour
    /// is (`FacetsRecipe.primaryColor`), read directly rather than through a mesh.
    @Test("The same filters and hue always yield the same solid colour")
    func solidColorIsDeterministic() {
        let filters = BoardThemeFilters(tone: .dark, colors: .duo, mesh: .fine, saturation: .rich)
        let first = filters.recipe(hue: .teal, seed: 0).primaryColor
        let second = filters.recipe(hue: .teal, seed: 0).primaryColor

        #expect(first == second)
    }

    /// **Seed-independence**: `primaryColor` never reads `seed`, so a Solid swatch clicked at seed
    /// `0` (`BoardThemeTabView.applySolid`) is exactly what a Pattern swatch's ground colour would be
    /// at any seed — the fact that lets Solid color skip minting a seed at all.
    @Test("primaryColor is the same colour whatever the seed")
    func primaryColorIsSeedIndependent() {
        let filters = BoardThemeFilters(tone: .light, colors: .trio, mesh: .coarse, saturation: .soft)
        let seeds: [UInt64] = [0, 1, 42, .max]

        let colors = seeds.map { filters.recipe(hue: .amber, seed: $0).primaryColor }
        #expect(Set(colors).count == 1)
    }

    /// Different hues under the same filters still land on different solid colours — the carousel
    /// would otherwise show eight identical swatches.
    @Test("Different hues yield different solid colours under the same filters")
    func differentHuesYieldDifferentSolidColors() {
        let filters = BoardThemeFilters.initial(colorScheme: .light)
        let sky = filters.recipe(hue: .sky, seed: 0).primaryColor
        let rose = filters.recipe(hue: .rose, seed: 0).primaryColor

        #expect(sky != rose)
    }
}
