1package entity
2
3import (
4 "testing"
5)
6
7// TestValidateHexColor_ValidHex tests that standard hex colors are recognized.
8func TestValidateHexColor_ValidHex(t *testing.T) {
9 valid := []string{
10 "#000000",
11 "#ffffff",
12 "#0a0a0b",
13 "#4ade80",
14 "#4A90E2",
15 "#FFA500",
16 "#9B59B6",
17 "#00D4AA",
18 }
19
20 for _, hex := range valid {
21 if !IsValidHex(hex) {
22 t.Errorf("expected %q to be a valid hex color", hex)
23 }
24 }
25}
26
27// TestValidateHexColor_InvalidHex tests that non-hex or malformed strings are rejected.
28func TestValidateHexColor_InvalidHex(t *testing.T) {
29 invalid := []string{
30 "",
31 "rgb(0,0,0)",
32 "#FFF", // 3-char shorthand
33 "#FFFFFFFF", // 8-char with alpha
34 "#GGGGGG", // non-hex chars
35 "red",
36 "#12", // too short
37 "#1234567", // odd length
38 " #ffffff ", // whitespace
39 "#ffffff ", // trailing space
40 }
41
42 for _, hex := range invalid {
43 if IsValidHex(hex) {
44 t.Errorf("expected %q to be invalid hex", hex)
45 }
46 }
47}
48
49// TestDefaultThemeFonts_ReturnsSaneDefaults verifies the default font configuration has non-empty,
50// sensible values.
51func TestDefaultThemeFonts_ReturnsSaneDefaults(t *testing.T) {
52 fonts := DefaultThemeFonts()
53
54 if fonts.SansFont != "Fira Sans" {
55 t.Errorf("expected SansFont Fira Sans, got %q", fonts.SansFont)
56 }
57 if fonts.SerifFont != "Fira Sans" {
58 t.Errorf("expected SerifFont Fira Sans, got %q", fonts.SerifFont)
59 }
60 if fonts.MonospaceFont != "Fira Code" {
61 t.Errorf("expected MonospaceFont Fira Code, got %q", fonts.MonospaceFont)
62 }
63 if fonts.GtkFont != "Adwaita Sans" {
64 t.Errorf("expected GtkFont Adwaita Sans, got %q", fonts.GtkFont)
65 }
66 if fonts.DefaultSize != 16 {
67 t.Errorf("expected DefaultSize 16, got %d", fonts.DefaultSize)
68 }
69}
70
71// TestResolvedThemePalette_MustBeCSSSafeHex ensures that every color field
72// in a ResolvedThemePalette passes hex validation.
73// This is the locked contract: all palette colors in ResolvedTheme are CSS-safe hex.
74func TestResolvedThemePalette_MustBeCSSSafeHex(t *testing.T) {
75 // A well-formed palette that SHOULD be produced by resolution.
76 validPalette := ResolvedThemePalette{
77 Background: "#0a0a0b",
78 Surface: "#1a1a1b",
79 SurfaceVariant: "#2d2d2d",
80 Text: "#ffffff",
81 Muted: "#909090",
82 Accent: "#4ade80",
83 Border: "#333333",
84 }
85
86 fields := map[string]string{
87 "Background": validPalette.Background,
88 "Surface": validPalette.Surface,
89 "SurfaceVariant": validPalette.SurfaceVariant,
90 "Text": validPalette.Text,
91 "Muted": validPalette.Muted,
92 "Accent": validPalette.Accent,
93 "Border": validPalette.Border,
94 }
95
96 for name, value := range fields {
97 if !IsValidHex(value) {
98 t.Errorf("ResolvedThemePalette.%s = %q is not a valid CSS-safe hex color", name, value)
99 }
100 }
101}
102
103// TestResolvedThemePalette_RejectsInvalidColors verifies that any non-hex color
104// in a palette is detectable by a validation helper.
105func TestResolvedThemePalette_RejectsInvalidColors(t *testing.T) {
106 invalidFields := []struct {
107 name string
108 value string
109 }{
110 {"Background", "rgb(0,0,0)"},
111 {"Background", "#F"},
112 {"Surface", "white"},
113 {"Text", "#GGGGGG"},
114 {"Accent", "hsl(0,0%,0%)"},
115 {"Border", ""},
116 }
117
118 for _, tc := range invalidFields {
119 if IsValidHex(tc.value) {
120 t.Errorf("field %q value %q should be rejected as invalid hex", tc.name, tc.value)
121 }
122 }
123}
124
125// TestThemeModeColors_DefaultsAreCSSSafe ensures the default mode colors are valid hex.
126func TestThemeModeColors_DefaultsAreCSSSafe(t *testing.T) {
127 // Per the existing ui/theme palette: defaults are #4A90E2, #FFA500, #9B59B6, #00D4AA
128 defaults := ThemeModeColors{
129 PaneMode: "#4A90E2",
130 TabMode: "#FFA500",
131 SessionMode: "#9B59B6",
132 ResizeMode: "#00D4AA",
133 VimMode: "#F7768E",
134 }
135
136 fields := map[string]string{
137 "PaneMode": defaults.PaneMode,
138 "TabMode": defaults.TabMode,
139 "SessionMode": defaults.SessionMode,
140 "ResizeMode": defaults.ResizeMode,
141 "VimMode": defaults.VimMode,
142 }
143
144 for name, value := range fields {
145 if !IsValidHex(value) {
146 t.Errorf("ThemeModeColors.%s = %q is not a valid CSS-safe hex color", name, value)
147 }
148 }
149}
150
151// TestResolvedTheme_StructFieldsExist is a compile-time guard and field-inspection test
152// to verify that the ResolvedTheme struct carries all required fields from the locked contract.
153func TestResolvedTheme_StructFieldsExist(t *testing.T) {
154 rt := ResolvedTheme{
155 LightPalette: ResolvedThemePalette{
156 Background: "#0a0a0b",
157 Surface: "#1a1a1b",
158 },
159 DarkPalette: ResolvedThemePalette{
160 Background: "#fafafa",
161 Surface: "#ffffff",
162 },
163 ActivePalette: ResolvedThemePalette{
164 Background: "#0a0a0b",
165 },
166 PrefersDark: true,
167 ColorSchemeSource: "adwaita",
168 ThemeSource: ThemeSourceMetadata{Kind: ThemeSourceConfig},
169 Fonts: DefaultThemeFonts(),
170 UIScale: 1.0,
171 ModeColors: ThemeModeColors{
172 PaneMode: "#4A90E2",
173 TabMode: "#FFA500",
174 SessionMode: "#9B59B6",
175 ResizeMode: "#00D4AA",
176 VimMode: "#F7768E",
177 },
178 Warnings: []ThemeWarning{
179 {Field: "external", Message: "malformed external theme, using last-good"},
180 },
181 }
182
183 // Verify the struct can be constructed and fields are accessible.
184 if !rt.PrefersDark {
185 t.Error("expected PrefersDark to be true")
186 }
187 if rt.ColorSchemeSource != "adwaita" {
188 t.Errorf("expected ColorSchemeSource 'adwaita', got %q", rt.ColorSchemeSource)
189 }
190 if rt.ThemeSource.Kind != ThemeSourceConfig {
191 t.Errorf("expected ThemeSource.Kind 'config', got %q", rt.ThemeSource.Kind)
192 }
193 if rt.UIScale != 1.0 {
194 t.Errorf("expected UIScale 1.0, got %f", rt.UIScale)
195 }
196 if len(rt.Warnings) != 1 {
197 t.Errorf("expected 1 warning, got %d", len(rt.Warnings))
198 }
199 if rt.Fonts.DefaultSize != 16 {
200 t.Errorf("expected DefaultSize 16, got %d", rt.Fonts.DefaultSize)
201 }
202}
203
204// TestThemeSourceKind_ConstantsAreDistinct verifies the ThemeSourceKind constants
205// have unique values so that source metadata comparisons work correctly.
206func TestThemeSourceKind_ConstantsAreDistinct(t *testing.T) {
207 kinds := map[ThemeSourceKind]bool{
208 ThemeSourceDefault: true,
209 ThemeSourceConfig: true,
210 ThemeSourceExternal: true,
211 }
212
213 if len(kinds) != 3 {
214 t.Error("ThemeSourceKind constants must be distinct")
215 }
216
217 if ThemeSourceDefault == ThemeSourceExternal {
218 t.Error("ThemeSourceDefault must not equal ThemeSourceExternal")
219 }
220 if ThemeSourceDefault == ThemeSourceConfig {
221 t.Error("ThemeSourceDefault must not equal ThemeSourceConfig")
222 }
223}
224
225// TestThemeWarning_FieldsExist verifies ThemeWarning carries the expected shape.
226func TestThemeWarning_FieldsExist(t *testing.T) {
227 w := ThemeWarning{
228 Field: "light_palette.background",
229 Message: "not a valid hex color",
230 }
231
232 if w.Field == "" {
233 t.Error("expected ThemeWarning.Field to be non-empty")
234 }
235 if w.Message == "" {
236 t.Error("expected ThemeWarning.Message to be non-empty")
237 }
238}
239
240// TestExternalTheme_IsDomainPure verifies ExternalTheme can be constructed without any
241// infrastructure imports (this is a compile-time contract via package structure).
242func TestExternalTheme_IsDomainPure(t *testing.T) {
243 light := ColorPalette{
244 Background: "#0a0a0b",
245 Surface: "#1a1a1b",
246 }
247 dark := ColorPalette{
248 Background: "#fafafa",
249 Surface: "#ffffff",
250 }
251
252 ext := ExternalTheme{
253 Name: "Noctalia Dark",
254 Provider: "noctalia",
255 LightPalette: &light,
256 DarkPalette: &dark,
257 }
258
259 if ext.Name != "Noctalia Dark" {
260 t.Errorf("expected Name 'Noctalia Dark', got %q", ext.Name)
261 }
262 if ext.Provider != "noctalia" {
263 t.Errorf("expected Provider 'noctalia', got %q", ext.Provider)
264 }
265 if ext.LightPalette == nil || ext.DarkPalette == nil {
266 t.Error("expected non-nil palettes")
267 }
268}
269
270// TestDefaultThemeFonts_AllFieldsAreSet ensures no zero-value fields in the defaults.
271func TestDefaultThemeFonts_AllFieldsAreSet(t *testing.T) {
272 fonts := DefaultThemeFonts()
273
274 // Check that all string fields are non-empty
275 if fonts.SansFont == "" {
276 t.Error("SansFont should have a default value")
277 }
278 if fonts.SerifFont == "" {
279 t.Error("SerifFont should have a default value")
280 }
281 if fonts.MonospaceFont == "" {
282 t.Error("MonospaceFont should have a default value")
283 }
284 if fonts.GtkFont == "" {
285 t.Error("GtkFont should have a default value")
286 }
287 if fonts.DefaultSize <= 0 {
288 t.Errorf("DefaultSize should be positive, got %d", fonts.DefaultSize)
289 }
290}
291
292// TestDefaultThemeModeColors_ReturnsSaneDefaults verifies the default mode colors.
293func TestDefaultThemeModeColors_ReturnsSaneDefaults(t *testing.T) {
294 mc := DefaultThemeModeColors()
295
296 if mc.PaneMode != "#4A90E2" {
297 t.Errorf("expected PaneMode #4A90E2, got %q", mc.PaneMode)
298 }
299 if mc.TabMode != "#FFA500" {
300 t.Errorf("expected TabMode #FFA500, got %q", mc.TabMode)
301 }
302 if mc.SessionMode != "#9B59B6" {
303 t.Errorf("expected SessionMode #9B59B6, got %q", mc.SessionMode)
304 }
305 if mc.ResizeMode != "#00D4AA" {
306 t.Errorf("expected ResizeMode #00D4AA, got %q", mc.ResizeMode)
307 }
308
309 // All must be valid hex
310 if !IsValidHex(mc.PaneMode) {
311 t.Error("default PaneMode should be valid hex")
312 }
313 if !IsValidHex(mc.TabMode) {
314 t.Error("default TabMode should be valid hex")
315 }
316 if !IsValidHex(mc.SessionMode) {
317 t.Error("default SessionMode should be valid hex")
318 }
319 if !IsValidHex(mc.ResizeMode) {
320 t.Error("default ResizeMode should be valid hex")
321 }
322}
323
324// TestDefaultDarkPalette_ReturnsValidHex verifies the default dark palette is all valid hex.
325func TestDefaultDarkPalette_ReturnsValidHex(t *testing.T) {
326 p := DefaultDarkPalette()
327
328 fields := map[string]string{
329 "Background": p.Background,
330 "Surface": p.Surface,
331 "SurfaceVariant": p.SurfaceVariant,
332 "Text": p.Text,
333 "Muted": p.Muted,
334 "Accent": p.Accent,
335 "Border": p.Border,
336 }
337 for name, value := range fields {
338 if value == "" {
339 t.Errorf("DefaultDarkPalette.%s should not be empty", name)
340 }
341 if !IsValidHex(value) {
342 t.Errorf("DefaultDarkPalette.%s = %q is not valid hex", name, value)
343 }
344 }
345}
346
347// TestDefaultLightPalette_ReturnsValidHex verifies the default light palette is all valid hex.
348func TestDefaultLightPalette_ReturnsValidHex(t *testing.T) {
349 p := DefaultLightPalette()
350
351 fields := map[string]string{
352 "Background": p.Background,
353 "Surface": p.Surface,
354 "SurfaceVariant": p.SurfaceVariant,
355 "Text": p.Text,
356 "Muted": p.Muted,
357 "Accent": p.Accent,
358 "Border": p.Border,
359 }
360 for name, value := range fields {
361 if value == "" {
362 t.Errorf("DefaultLightPalette.%s should not be empty", name)
363 }
364 if !IsValidHex(value) {
365 t.Errorf("DefaultLightPalette.%s = %q is not valid hex", name, value)
366 }
367 }
368}
369
370// TestValidatePaletteHex_Valid verifies ValidatePaletteHex returns no warnings for valid palettes.
371func TestValidatePaletteHex_Valid(t *testing.T) {
372 p := DefaultDarkPalette()
373 warnings := ValidatePaletteHex(&p, "dark")
374 if len(warnings) != 0 {
375 t.Errorf("expected no warnings for valid palette, got %d: %v", len(warnings), warnings)
376 }
377}
378
379// TestValidatePaletteHex_Invalid verifies ValidatePaletteHex returns warnings for invalid palettes.
380func TestValidatePaletteHex_Invalid(t *testing.T) {
381 p := ColorPalette{
382 Background: "rgb(0,0,0)", // invalid
383 Surface: "#ffffff",
384 SurfaceVariant: "not-a-color",
385 Text: "", // invalid
386 Muted: "#GGGGGG", // invalid
387 Accent: "#123", // 3-char shorthand, rejected
388 Border: "#123456",
389 }
390 warnings := ValidatePaletteHex(&p, "test")
391 if len(warnings) == 0 {
392 t.Error("expected warnings for invalid palette, got none")
393 }
394}
395
396// TestValidatePaletteHex_Nil verifies ValidatePaletteHex handles nil.
397func TestValidatePaletteHex_Nil(t *testing.T) {
398 warnings := ValidatePaletteHex(nil, "ext")
399 if len(warnings) == 0 {
400 t.Error("expected warnings for nil palette")
401 }
402}
403
404// TestIsPaletteValid verifies IsPaletteValid returns correct truthiness.
405func TestIsPaletteValid_EdgeCases(t *testing.T) {
406 if IsPaletteValid(nil) {
407 t.Error("nil palette should not be valid")
408 }
409
410 valid := DefaultDarkPalette()
411 if !IsPaletteValid(&valid) {
412 t.Error("default dark palette should be valid")
413 }
414
415 partial := ColorPalette{
416 Background: "#000000",
417 Surface: "#111111",
418 // missing fields are ""
419 }
420 if IsPaletteValid(&partial) {
421 t.Error("partial palette with empty fields should not be valid")
422 }
423}
424
425// TestMergePalette_FillsEmpties verifies MergePalette fills empty fields from defaults.
426func TestMergePalette_FillsEmpties(t *testing.T) {
427 src := &ColorPalette{
428 Background: "#000000",
429 Surface: "", // should be filled
430 Text: "#ffffff",
431 }
432 def := DefaultDarkPalette()
433 result := MergePalette(src, &def)
434
435 if result.Background != "#000000" {
436 t.Errorf("Background should be from src, got %q", result.Background)
437 }
438 if result.Surface != def.Surface {
439 t.Errorf("Surface should be filled from defaults, got %q", result.Surface)
440 }
441 if result.Text != "#ffffff" {
442 t.Errorf("Text should be from src, got %q", result.Text)
443 }
444}
445
446// TestMergePalette_NilSrc verifies MergePalette returns defaults when src is nil.
447func TestMergePalette_NilSrc(t *testing.T) {
448 def := DefaultDarkPalette()
449 result := MergePalette(nil, &def)
450
451 if result.Background != def.Background {
452 t.Errorf("expected default Background, got %q", result.Background)
453 }
454}
455
456// TestMergeFonts_FillsEmpties verifies MergeFonts fills zero fields from defaults.
457func TestMergeFonts_FillsEmpties(t *testing.T) {
458 src := &ThemeFonts{
459 SansFont: "Custom Sans",
460 // SerifFont empty
461 DefaultSize: 0, // zero
462 }
463 def := DefaultThemeFonts()
464 result := MergeFonts(src, def)
465
466 if result.SansFont != "Custom Sans" {
467 t.Errorf("SansFont should be from src, got %q", result.SansFont)
468 }
469 if result.SerifFont != def.SerifFont {
470 t.Errorf("SerifFont should be from defaults, got %q", result.SerifFont)
471 }
472 if result.DefaultSize != def.DefaultSize {
473 t.Errorf("DefaultSize should be from defaults, got %d", result.DefaultSize)
474 }
475}
476
477// TestMergeFonts_NilSrc verifies MergeFonts returns defaults when src is nil.
478func TestMergeFonts_NilSrc(t *testing.T) {
479 def := DefaultThemeFonts()
480 result := MergeFonts(nil, def)
481
482 if result.SansFont != def.SansFont {
483 t.Errorf("expected default SansFont, got %q", result.SansFont)
484 }
485}
486
487// TestMergeModeColors_FillsEmpties verifies mode color merging.
488func TestMergeModeColors_FillsEmpties(t *testing.T) {
489 src := &ThemeModeColors{
490 PaneMode: "#111111",
491 // TabMode empty
492 }
493 def := DefaultThemeModeColors()
494 result := MergeModeColors(src, def)
495
496 if result.PaneMode != "#111111" {
497 t.Errorf("PaneMode should be from src, got %q", result.PaneMode)
498 }
499 if result.VimMode != def.VimMode {
500 t.Errorf("VimMode should use default, got %q", result.VimMode)
501 }
502 if result.TabMode != def.TabMode {
503 t.Errorf("TabMode should be from defaults, got %q", result.TabMode)
504 }
505}
506
507// TestValidateModeColorsHex_ReturnsWarnings verifies invalid mode colors are detected.
508func TestValidateModeColorsHex_ReturnsWarnings(t *testing.T) {
509 colors := &ThemeModeColors{
510 PaneMode: "#111111",
511 TabMode: "not-a-color",
512 SessionMode: "#333333",
513 ResizeMode: "",
514 VimMode: "#F7768E",
515 }
516
517 warnings := ValidateModeColorsHex(colors, "mode_colors")
518 if len(warnings) != 2 {
519 t.Fatalf("expected 2 invalid mode color warnings, got %d: %+v", len(warnings), warnings)
520 }
521 if IsModeColorsValid(colors) {
522 t.Error("expected invalid mode colors to be rejected")
523 }
524}
525
526// TestValidateModeColorsHex_DefaultsAreValid verifies default mode colors satisfy validation.
527func TestValidateModeColorsHex_DefaultsAreValid(t *testing.T) {
528 defaults := DefaultThemeModeColors()
529 if warnings := ValidateModeColorsHex(&defaults, "mode_colors"); len(warnings) != 0 {
530 t.Fatalf("expected default mode colors to be valid, got warnings: %+v", warnings)
531 }
532 if !IsModeColorsValid(&defaults) {
533 t.Error("expected default mode colors to be valid")
534 }
535}