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 i := 0; i < goroutines; i++ {
359 wg.Add(1)
360 go func() {
361 defer wg.Done()
362 for j := 0; j < 100; j++ {
363 resolver.Resolve()
364 }
365 }()
366 }
367
368 // Concurrent Refresh calls
369 for i := 0; i < goroutines; i++ {
370 wg.Add(1)
371 go func() {
372 defer wg.Done()
373 for j := 0; j < 100; j++ {
374 resolver.Refresh()
375 }
376 }()
377 }
378
379 // Concurrent RegisterDetector calls
380 for i := 0; i < goroutines; i++ {
381 wg.Add(1)
382 go func(id int) {
383 defer wg.Done()
384 resolver.RegisterDetector(&mockDetector{
385 name: "concurrent",
386 priority: id,
387 available: true,
388 prefersDark: id%2 == 0,
389 detectOk: true,
390 })
391 }(i)
392 }
393
394 wg.Wait()
395 // Test passes if no race conditions detected
396}
397
398func TestResolver_EmptyConfigScheme(t *testing.T) {
399 config := &mockConfigProvider{scheme: ""}
400 resolver := NewResolver(config)
401
402 detector := &mockDetector{
403 name: "test",
404 priority: 50,
405 available: true,
406 prefersDark: false,
407 detectOk: true,
408 }
409 resolver.RegisterDetector(detector)
410
411 pref := resolver.Resolve()
412
413 // Empty config should fall through to detector
414 assert.False(t, pref.PrefersDark)
415 assert.Equal(t, "test", pref.Source)
416}
417
418func TestResolver_ImplementsInterface(t *testing.T) {
419 config := &mockConfigProvider{scheme: "default"}
420 resolver := NewResolver(config)
421
422 // Verify Resolver implements port.ColorSchemeResolver
423 var _ port.ColorSchemeResolver = resolver
424 require.NotNil(t, resolver)
425}