1package colorscheme
2
3import (
4 "sync"
5 "testing"
6
7 "github.com/bnema/dumber/internal/application/port"
8 "github.com/stretchr/testify/assert"
9 "github.com/stretchr/testify/require"
10)
11
12// mockConfigProvider implements ConfigProvider for testing.
13type mockConfigProvider struct {
14 scheme string
15}
16
17func (m *mockConfigProvider) GetColorScheme() string {
18 return m.scheme
19}
20
21// mockDetector implements port.ColorSchemeDetector for testing.
22type mockDetector struct {
23 name string
24 priority int
25 available bool
26 prefersDark bool
27 detectOk bool
28}
29
30func (m *mockDetector) Name() string { return m.name }
31func (m *mockDetector) Priority() int { return m.priority }
32func (m *mockDetector) Available() bool { return m.available }
33func (m *mockDetector) Detect() (bool, bool) { return m.prefersDark, m.detectOk }
34
35func TestResolver_ConfigOverride(t *testing.T) {
36 tests := []struct {
37 name string
38 configValue string
39 wantDark bool
40 wantSource string
41 }{
42 {
43 name: "prefer-dark from config",
44 configValue: "prefer-dark",
45 wantDark: true,
46 wantSource: "config",
47 },
48 {
49 name: "dark from config",
50 configValue: "dark",
51 wantDark: true,
52 wantSource: "config",
53 },
54 {
55 name: "prefer-light from config",
56 configValue: "prefer-light",
57 wantDark: false,
58 wantSource: "config",
59 },
60 {
61 name: "light from config",
62 configValue: "light",
63 wantDark: false,
64 wantSource: "config",
65 },
66 }
67
68 for _, tt := range tests {
69 t.Run(tt.name, func(t *testing.T) {
70 config := &mockConfigProvider{scheme: tt.configValue}
71 resolver := NewResolver(config)
72
73 pref := resolver.Resolve()
74
75 assert.Equal(t, tt.wantDark, pref.PrefersDark)
76 assert.Equal(t, tt.wantSource, pref.Source)
77 })
78 }
79}
80
81func TestResolver_DefaultFallsThrough(t *testing.T) {
82 config := &mockConfigProvider{scheme: "default"}
83 resolver := NewResolver(config)
84
85 // Add a detector that returns light
86 detector := &mockDetector{
87 name: "test",
88 priority: 50,
89 available: true,
90 prefersDark: false,
91 detectOk: true,
92 }
93 resolver.RegisterDetector(detector)
94
95 pref := resolver.Resolve()
96
97 assert.False(t, pref.PrefersDark)
98 assert.Equal(t, "test", pref.Source)
99}
100
101func TestResolver_DetectorPriority(t *testing.T) {
102 config := &mockConfigProvider{scheme: "default"}
103 resolver := NewResolver(config)
104
105 // Low priority detector returns dark
106 lowPriority := &mockDetector{
107 name: "low",
108 priority: 10,
109 available: true,
110 prefersDark: true,
111 detectOk: true,
112 }
113 // High priority detector returns light
114 highPriority := &mockDetector{
115 name: "high",
116 priority: 100,
117 available: true,
118 prefersDark: false,
119 detectOk: true,
120 }
121
122 // Register low first, high second (order shouldn't matter)
123 resolver.RegisterDetector(lowPriority)
124 resolver.RegisterDetector(highPriority)
125
126 pref := resolver.Resolve()
127
128 // Should use high priority detector
129 assert.False(t, pref.PrefersDark)
130 assert.Equal(t, "high", pref.Source)
131}
132
133func TestResolver_SkipsUnavailableDetector(t *testing.T) {
134 config := &mockConfigProvider{scheme: "default"}
135 resolver := NewResolver(config)
136
137 // High priority but unavailable
138 unavailable := &mockDetector{
139 name: "unavailable",
140 priority: 100,
141 available: false,
142 prefersDark: false,
143 detectOk: true,
144 }
145 // Low priority but available
146 available := &mockDetector{
147 name: "available",
148 priority: 10,
149 available: true,
150 prefersDark: true,
151 detectOk: true,
152 }
153
154 resolver.RegisterDetector(unavailable)
155 resolver.RegisterDetector(available)
156
157 pref := resolver.Resolve()
158
159 // Should use available detector
160 assert.True(t, pref.PrefersDark)
161 assert.Equal(t, "available", pref.Source)
162}
163
164func TestResolver_SkipsFailedDetection(t *testing.T) {
165 config := &mockConfigProvider{scheme: "default"}
166 resolver := NewResolver(config)
167
168 // High priority but detection fails
169 failing := &mockDetector{
170 name: "failing",
171 priority: 100,
172 available: true,
173 prefersDark: false,
174 detectOk: false, // Detection fails
175 }
176 // Low priority but succeeds
177 succeeding := &mockDetector{
178 name: "succeeding",
179 priority: 10,
180 available: true,
181 prefersDark: true,
182 detectOk: true,
183 }
184
185 resolver.RegisterDetector(failing)
186 resolver.RegisterDetector(succeeding)
187
188 pref := resolver.Resolve()
189
190 // Should use succeeding detector
191 assert.True(t, pref.PrefersDark)
192 assert.Equal(t, "succeeding", pref.Source)
193}
194
195func TestResolver_FallbackWhenNoDetectors(t *testing.T) {
196 config := &mockConfigProvider{scheme: "default"}
197 resolver := NewResolver(config)
198
199 // No detectors registered
200 pref := resolver.Resolve()
201
202 // Should fallback to dark
203 assert.True(t, pref.PrefersDark)
204 assert.Equal(t, "fallback", pref.Source)
205}
206
207func TestResolver_FallbackWhenAllFail(t *testing.T) {
208 config := &mockConfigProvider{scheme: "default"}
209 resolver := NewResolver(config)
210
211 // All detectors fail
212 resolver.RegisterDetector(&mockDetector{
213 name: "fail1",
214 priority: 100,
215 available: true,
216 detectOk: false,
217 })
218 resolver.RegisterDetector(&mockDetector{
219 name: "fail2",
220 priority: 50,
221 available: false,
222 })
223
224 pref := resolver.Resolve()
225
226 // Should fallback to dark
227 assert.True(t, pref.PrefersDark)
228 assert.Equal(t, "fallback", pref.Source)
229}
230
231func TestResolver_NilConfig(t *testing.T) {
232 resolver := NewResolver(nil)
233
234 detector := &mockDetector{
235 name: "test",
236 priority: 50,
237 available: true,
238 prefersDark: false,
239 detectOk: true,
240 }
241 resolver.RegisterDetector(detector)
242
243 pref := resolver.Resolve()
244
245 // Should use detector since config is nil
246 assert.False(t, pref.PrefersDark)
247 assert.Equal(t, "test", pref.Source)
248}
249
250func TestResolver_Refresh(t *testing.T) {
251 config := &mockConfigProvider{scheme: "default"}
252 resolver := NewResolver(config)
253
254 // Start with light-preferring detector
255 detector := &mockDetector{
256 name: "test",
257 priority: 50,
258 available: true,
259 prefersDark: false,
260 detectOk: true,
261 }
262 resolver.RegisterDetector(detector)
263
264 pref1 := resolver.Refresh()
265 assert.False(t, pref1.PrefersDark)
266
267 // Change detector preference
268 detector.prefersDark = true
269
270 pref2 := resolver.Refresh()
271 assert.True(t, pref2.PrefersDark)
272}
273
274func TestResolver_OnChange(t *testing.T) {
275 config := &mockConfigProvider{scheme: "default"}
276 resolver := NewResolver(config)
277
278 detector := &mockDetector{
279 name: "test",
280 priority: 50,
281 available: true,
282 prefersDark: false,
283 detectOk: true,
284 }
285 resolver.RegisterDetector(detector)
286
287 var callbackPref port.ColorSchemePreference
288 var callbackCount int
289 resolver.OnChange(func(pref port.ColorSchemePreference) {
290 callbackPref = pref
291 callbackCount++
292 })
293
294 // Initial refresh - callback should be called (default was dark, now light)
295 resolver.Refresh()
296 assert.Equal(t, 1, callbackCount)
297 assert.False(t, callbackPref.PrefersDark)
298
299 // Same preference - callback should NOT be called
300 resolver.Refresh()
301 assert.Equal(t, 1, callbackCount)
302
303 // Change preference - callback should be called
304 detector.prefersDark = true
305 resolver.Refresh()
306 assert.Equal(t, 2, callbackCount)
307 assert.True(t, callbackPref.PrefersDark)
308}
309
310func TestResolver_OnChangeUnregister(t *testing.T) {
311 config := &mockConfigProvider{scheme: "default"}
312 resolver := NewResolver(config)
313
314 detector := &mockDetector{
315 name: "test",
316 priority: 50,
317 available: true,
318 prefersDark: false,
319 detectOk: true,
320 }
321 resolver.RegisterDetector(detector)
322
323 var callbackCount int
324 unregister := resolver.OnChange(func(_ port.ColorSchemePreference) {
325 callbackCount++
326 })
327
328 // Initial refresh triggers callback
329 resolver.Refresh()
330 assert.Equal(t, 1, callbackCount)
331
332 // Unregister
333 unregister()
334
335 // Change preference - callback should NOT be called
336 detector.prefersDark = true
337 resolver.Refresh()
338 assert.Equal(t, 1, callbackCount) // Still 1, not 2
339}
340
341func TestResolver_ConcurrentAccess(_ *testing.T) {
342 config := &mockConfigProvider{scheme: "default"}
343 resolver := NewResolver(config)
344
345 detector := &mockDetector{
346 name: "test",
347 priority: 50,
348 available: true,
349 prefersDark: false,
350 detectOk: true,
351 }
352 resolver.RegisterDetector(detector)
353
354 var wg sync.WaitGroup
355 const goroutines = 10
356
357 // Concurrent Resolve calls
358 for range goroutines {
359 wg.Go(func() {
360 for range 100 {
361 resolver.Resolve()
362 }
363 })
364 }
365
366 // Concurrent Refresh calls
367 for range goroutines {
368 wg.Go(func() {
369 for range 100 {
370 resolver.Refresh()
371 }
372 })
373 }
374
375 // Concurrent RegisterDetector calls
376 for i := range goroutines {
377 wg.Add(1)
378 go func(id int) {
379 defer wg.Done()
380 resolver.RegisterDetector(&mockDetector{
381 name: "concurrent",
382 priority: id,
383 available: true,
384 prefersDark: id%2 == 0,
385 detectOk: true,
386 })
387 }(i)
388 }
389
390 wg.Wait()
391 // Test passes if no race conditions detected
392}
393
394func TestResolver_EmptyConfigScheme(t *testing.T) {
395 config := &mockConfigProvider{scheme: ""}
396 resolver := NewResolver(config)
397
398 detector := &mockDetector{
399 name: "test",
400 priority: 50,
401 available: true,
402 prefersDark: false,
403 detectOk: true,
404 }
405 resolver.RegisterDetector(detector)
406
407 pref := resolver.Resolve()
408
409 // Empty config should fall through to detector
410 assert.False(t, pref.PrefersDark)
411 assert.Equal(t, "test", pref.Source)
412}
413
414func TestResolver_ImplementsInterface(t *testing.T) {
415 config := &mockConfigProvider{scheme: "default"}
416 resolver := NewResolver(config)
417
418 // Verify Resolver implements port.ColorSchemeResolver
419 var _ port.ColorSchemeResolver = resolver
420 require.NotNil(t, resolver)
421}