1+ using System . Runtime . Versioning ;
2+
3+ using LogExpert . UI . Services ;
4+
5+ using NUnit . Framework ;
6+
7+ namespace LogExpert . Tests . Services ;
8+
9+ [ TestFixture ]
10+ [ Apartment ( ApartmentState . STA ) ] // Required for UI components
11+ [ SupportedOSPlatform ( "windows" ) ]
12+ public class LedIndicatorServiceTests
13+ {
14+ private LedIndicatorService ? _service ;
15+ private ApplicationContext ? _appContext ;
16+
17+ [ SetUp ]
18+ public void Setup ( )
19+ {
20+ // Ensure we have a WindowsFormsSynchronizationContext for the UI thread
21+ if ( SynchronizationContext . Current == null )
22+ {
23+ SynchronizationContext . SetSynchronizationContext ( new WindowsFormsSynchronizationContext ( ) ) ;
24+ }
25+
26+ // Create an application context to ensure we have a proper UI context
27+ _appContext = new ApplicationContext ( ) ;
28+
29+ // Must be created on STA thread with synchronization context
30+ _service = new LedIndicatorService ( ) ;
31+ }
32+
33+ [ TearDown ]
34+ public void TearDown ( )
35+ {
36+ _service ? . Dispose ( ) ;
37+ _appContext ? . Dispose ( ) ;
38+ }
39+
40+ [ Test ]
41+ public void Initialize_WithValidColor_Succeeds ( )
42+ {
43+ // Act
44+ _service ! . Initialize ( Color . Blue ) ;
45+
46+ // Assert - no exception thrown
47+ Assert . That ( _service , Is . Not . Null ) ;
48+ }
49+
50+ [ Test ]
51+ public void Initialize_CalledTwice_ThrowsException ( )
52+ {
53+ // Arrange
54+ _service ! . Initialize ( Color . Blue ) ;
55+
56+ // Act & Assert
57+ _ = Assert . Throws < InvalidOperationException > ( ( ) => _service . Initialize ( Color . Red ) ) ;
58+ }
59+
60+ [ Test ]
61+ public void GetIcon_WithZeroDiff_ReturnsOffLevelIcon ( )
62+ {
63+ // Arrange
64+ _service ! . Initialize ( Color . Blue ) ;
65+ var state = new LedState
66+ {
67+ IsDirty = false ,
68+ TailState = TailFollowState . On ,
69+ SyncState = TimeSyncState . NotSynced
70+ } ;
71+
72+ // Act
73+ var icon = _service . GetIcon ( 0 , state ) ;
74+
75+ // Assert
76+ Assert . That ( icon , Is . Not . Null ) ;
77+ Assert . That ( icon . Width , Is . EqualTo ( 16 ) ) ;
78+ Assert . That ( icon . Height , Is . EqualTo ( 16 ) ) ;
79+ }
80+
81+ [ Test ]
82+ public void GetIcon_WithMaxDiff_ReturnsHighestLevelIcon ( )
83+ {
84+ // Arrange
85+ _service ! . Initialize ( Color . Blue ) ;
86+ var state = new LedState
87+ {
88+ IsDirty = false ,
89+ TailState = TailFollowState . On ,
90+ SyncState = TimeSyncState . NotSynced
91+ } ;
92+
93+ // Act
94+ var icon = _service . GetIcon ( 100 , state ) ;
95+
96+ // Assert
97+ Assert . That ( icon , Is . Not . Null ) ;
98+ }
99+
100+ [ Test ]
101+ public void GetIcon_WithDirtyState_ReturnsDirtyIcon ( )
102+ {
103+ // Arrange
104+ _service ! . Initialize ( Color . Blue ) ;
105+ var state = new LedState
106+ {
107+ IsDirty = true ,
108+ TailState = TailFollowState . On ,
109+ SyncState = TimeSyncState . NotSynced
110+ } ;
111+
112+ // Act
113+ var icon = _service . GetIcon ( 50 , state ) ;
114+
115+ // Assert
116+ Assert . That ( icon , Is . Not . Null ) ;
117+ }
118+
119+ [ Test ]
120+ public void GetDeadIcon_ReturnsNonNullIcon ( )
121+ {
122+ // Arrange
123+ _service ! . Initialize ( Color . Blue ) ;
124+
125+ // Act
126+ var icon = _service . GetDeadIcon ( ) ;
127+
128+ // Assert
129+ Assert . That ( icon , Is . Not . Null ) ;
130+ Assert . That ( icon . Width , Is . EqualTo ( 16 ) ) ;
131+ }
132+
133+ [ Test ]
134+ public void StartStop_DoesNotThrowException ( )
135+ {
136+ // Arrange
137+ _service ! . Initialize ( Color . Blue ) ;
138+
139+ // Act
140+ _service . Start ( ) ;
141+ Thread . Sleep ( 500 ) ; // Let timer tick a few times
142+ _service . Stop ( ) ;
143+
144+ // Assert - no exception
145+ Assert . That ( true , Is . True , "Service started and stopped without exceptions" ) ;
146+ }
147+
148+ [ Test ]
149+ public void RegisterWindow_AddsWindowToTracking ( )
150+ {
151+ // Arrange
152+ _service ! . Initialize ( Color . Blue ) ;
153+
154+ // We can't easily mock LogWindow since it has no parameterless constructor
155+ // and is internal, so we just test that registering null throws
156+ // Act & Assert
157+ _ = Assert . Throws < ArgumentNullException > ( ( ) => _service . RegisterWindow ( null ! ) ) ;
158+ }
159+
160+ [ Test ]
161+ public void UpdateWindowActivity_WithoutRegisteringWindow_DoesNotThrow ( )
162+ {
163+ // Arrange
164+ _service ! . Initialize ( Color . Blue ) ;
165+
166+ // Act & Assert - Updating an unregistered window should not throw
167+ // (it just won't raise events)
168+ Assert . DoesNotThrow ( ( ) => _service . UpdateWindowActivity ( null , 10 ) ) ;
169+ }
170+
171+ [ Test ]
172+ public void RegenerateIcons_WithNoWindows_DoesNotThrow ( )
173+ {
174+ // Arrange
175+ _service ! . Initialize ( Color . Blue ) ;
176+
177+ int eventCount = 0 ;
178+ _service . IconChanged += ( s , e ) => eventCount ++ ;
179+
180+ // Act
181+ _service . RegenerateIcons ( Color . Red ) ;
182+
183+ // Assert - No windows registered, so no events should be raised
184+ Assert . That ( eventCount , Is . EqualTo ( 0 ) ) ;
185+ }
186+
187+ [ Test ]
188+ public void Dispose_DisposesAllResources ( )
189+ {
190+ // Arrange
191+ _service ! . Initialize ( Color . Blue ) ;
192+ _service . Start ( ) ;
193+
194+ // Act
195+ _service . Dispose ( ) ;
196+
197+ // Assert - After dispose, trying to use the service will throw an exception
198+ var exception = Assert . Catch ( ( ) => _service . GetIcon ( 0 , new LedState ( ) ) ) ;
199+ Assert . That ( exception , Is . Not . Null , "Should throw an exception after disposal" ) ;
200+ }
201+
202+ [ Test ]
203+ public void GetIcon_WithoutInitialize_ThrowsException ( )
204+ {
205+ // Arrange - don't initialize
206+
207+ // Act & Assert
208+ _ = Assert . Throws < InvalidOperationException > ( ( ) => _service ! . GetIcon ( 0 , new LedState ( ) ) ) ;
209+ }
210+
211+ [ Test ]
212+ public void Start_WithoutInitialize_ThrowsException ( )
213+ {
214+ // Arrange - don't initialize
215+
216+ // Act & Assert
217+ _ = Assert . Throws < InvalidOperationException > ( ( ) => _service ! . Start ( ) ) ;
218+ }
219+
220+ [ Test ]
221+ public void RegisterWindow_WithNullWindow_ThrowsException ( )
222+ {
223+ // Arrange
224+ _service ! . Initialize ( Color . Blue ) ;
225+
226+ // Act & Assert
227+ _ = Assert . Throws < ArgumentNullException > ( ( ) => _service . RegisterWindow ( null ! ) ) ;
228+ }
229+
230+ [ Test ]
231+ public void UnregisterWindow_WithNullWindow_DoesNotThrow ( )
232+ {
233+ // Arrange
234+ _service ! . Initialize ( Color . Blue ) ;
235+
236+ // Act & Assert - Unregistering null should not throw
237+ Assert . DoesNotThrow ( ( ) => _service . UnregisterWindow ( null ) ) ;
238+ }
239+
240+ [ Test ]
241+ public void CurrentTailColor_AfterInitialize_ReturnsInitializedColor ( )
242+ {
243+ // Arrange
244+ var expectedColor = Color . FromArgb ( 50 , 100 , 200 ) ;
245+ _service ! . Initialize ( expectedColor ) ;
246+
247+ // Act
248+ var actualColor = _service . CurrentTailColor ;
249+
250+ // Assert
251+ Assert . That ( actualColor , Is . EqualTo ( expectedColor ) ) ;
252+ }
253+
254+ [ Test ]
255+ public void CurrentTailColor_AfterRegenerateIcons_ReturnsNewColor ( )
256+ {
257+ // Arrange
258+ _service ! . Initialize ( Color . Blue ) ;
259+ var newColor = Color . FromArgb ( 255 , 128 , 0 ) ;
260+
261+ // Act
262+ _service . RegenerateIcons ( newColor ) ;
263+
264+ // Assert
265+ Assert . That ( _service . CurrentTailColor , Is . EqualTo ( newColor ) ) ;
266+ }
267+
268+ [ Test ]
269+ public void CurrentTailColor_BeforeInitialize_ThrowsException ( )
270+ {
271+ // Arrange - don't initialize
272+
273+ // Act & Assert
274+ _ = Assert . Throws < InvalidOperationException > ( ( ) => _ = _service ! . CurrentTailColor ) ;
275+ }
276+
277+ [ Test ]
278+ public void CurrentTailColor_AfterDispose_ThrowsObjectDisposedException ( )
279+ {
280+ // Arrange
281+ _service ! . Initialize ( Color . Blue ) ;
282+ _service . Dispose ( ) ;
283+
284+ // Act & Assert
285+ Assert . Throws < ObjectDisposedException > ( ( ) => _ = _service . CurrentTailColor ) ;
286+ }
287+
288+ [ Test ]
289+ public void GetIcon_WithSyncedState_ReturnsSyncedIcon ( )
290+ {
291+ // Arrange
292+ _service ! . Initialize ( Color . Blue ) ;
293+ var stateSynced = new LedState
294+ {
295+ IsDirty = false ,
296+ TailState = TailFollowState . On ,
297+ SyncState = TimeSyncState . Synced
298+ } ;
299+ var stateNotSynced = new LedState
300+ {
301+ IsDirty = false ,
302+ TailState = TailFollowState . On ,
303+ SyncState = TimeSyncState . NotSynced
304+ } ;
305+
306+ // Act
307+ var iconSynced = _service . GetIcon ( 50 , stateSynced ) ;
308+ var iconNotSynced = _service . GetIcon ( 50 , stateNotSynced ) ;
309+
310+ // Assert
311+ Assert . That ( iconSynced , Is . Not . Null ) ;
312+ Assert . That ( iconNotSynced , Is . Not . Null ) ;
313+ // The icons should be different (synced has blue indicator on left side)
314+ Assert . That ( iconSynced , Is . Not . EqualTo ( iconNotSynced ) ) ;
315+ }
316+ }
0 commit comments