/ GUNRPG.Tests / OperatorAggregateTests.cs
OperatorAggregateTests.cs
  1  using GUNRPG.Core.Operators;
  2  using Xunit;
  3  
  4  namespace GUNRPG.Tests;
  5  
  6  public class OperatorAggregateTests
  7  {
  8      [Fact]
  9      public void Create_ShouldInitializeFromCreatedEvent()
 10      {
 11          // Arrange
 12          var operatorId = OperatorId.NewId();
 13          var createdEvent = new OperatorCreatedEvent(operatorId, "TestOperator");
 14  
 15          // Act
 16          var aggregate = OperatorAggregate.Create(createdEvent);
 17  
 18          // Assert
 19          Assert.Equal(operatorId, aggregate.Id);
 20          Assert.Equal("TestOperator", aggregate.Name);
 21          Assert.Equal(0, aggregate.TotalXp);
 22          Assert.Equal(100f, aggregate.MaxHealth);
 23          Assert.Equal(100f, aggregate.CurrentHealth);
 24          Assert.Equal(0, aggregate.CurrentSequence);
 25      }
 26  
 27      [Fact]
 28      public void FromEvents_ShouldReplayEventsInOrder()
 29      {
 30          // Arrange
 31          var operatorId = OperatorId.NewId();
 32          var evt1 = new OperatorCreatedEvent(operatorId, "TestOperator");
 33          var evt2 = new XpGainedEvent(operatorId, 1, 100, "Victory", evt1.Hash);
 34          var evt3 = new XpGainedEvent(operatorId, 2, 50, "Survived", evt2.Hash);
 35          var events = new List<OperatorEvent> { evt1, evt2, evt3 };
 36  
 37          // Act
 38          var aggregate = OperatorAggregate.FromEvents(events);
 39  
 40          // Assert
 41          Assert.Equal(150, aggregate.TotalXp);
 42          Assert.Equal(2, aggregate.CurrentSequence);
 43      }
 44  
 45      [Fact]
 46      public void FromEvents_ShouldThrowOnEmptyEventList()
 47      {
 48          // Arrange
 49          var events = new List<OperatorEvent>();
 50  
 51          // Act & Assert
 52          var ex = Assert.Throws<InvalidOperationException>(() => OperatorAggregate.FromEvents(events));
 53          Assert.Contains("empty event list", ex.Message);
 54      }
 55  
 56      [Fact]
 57      public void FromEvents_ShouldRollbackOnBrokenChain()
 58      {
 59          // Arrange
 60          var operatorId = OperatorId.NewId();
 61          var evt1 = new OperatorCreatedEvent(operatorId, "TestOperator");
 62          var evt2 = new XpGainedEvent(operatorId, 1, 100, "Victory", "wrong_hash");
 63          var events = new List<OperatorEvent> { evt1, evt2 };
 64  
 65          // Act - Should roll back to evt1 only
 66          var aggregate = OperatorAggregate.FromEvents(events);
 67  
 68          // Assert - Only first event should be applied
 69          Assert.Equal(0, aggregate.TotalXp);
 70          Assert.Equal(0, aggregate.CurrentSequence);
 71      }
 72  
 73      [Fact]
 74      public void FromEvents_ShouldApplyXpGainedCorrectly()
 75      {
 76          // Arrange
 77          var operatorId = OperatorId.NewId();
 78          var evt1 = new OperatorCreatedEvent(operatorId, "TestOperator");
 79          var evt2 = new XpGainedEvent(operatorId, 1, 250, "Victory", evt1.Hash);
 80          var events = new List<OperatorEvent> { evt1, evt2 };
 81  
 82          // Act
 83          var aggregate = OperatorAggregate.FromEvents(events);
 84  
 85          // Assert
 86          Assert.Equal(250, aggregate.TotalXp);
 87      }
 88  
 89      [Fact]
 90      public void FromEvents_ShouldApplyWoundsTreatedCorrectly()
 91      {
 92          // Arrange
 93          var operatorId = OperatorId.NewId();
 94          var evt1 = new OperatorCreatedEvent(operatorId, "TestOperator");
 95          var evt2 = new WoundsTreatedEvent(operatorId, 1, 30f, evt1.Hash);
 96          var events = new List<OperatorEvent> { evt1, evt2 };
 97  
 98          // Act
 99          var aggregate = OperatorAggregate.FromEvents(events);
100  
101          // Assert
102          Assert.Equal(100f, aggregate.CurrentHealth); // Already at max
103      }
104  
105      [Fact]
106      public void FromEvents_ShouldApplyLoadoutChangedCorrectly()
107      {
108          // Arrange
109          var operatorId = OperatorId.NewId();
110          var evt1 = new OperatorCreatedEvent(operatorId, "TestOperator");
111          var evt2 = new LoadoutChangedEvent(operatorId, 1, "M4A1", evt1.Hash);
112          var events = new List<OperatorEvent> { evt1, evt2 };
113  
114          // Act
115          var aggregate = OperatorAggregate.FromEvents(events);
116  
117          // Assert
118          Assert.Equal("M4A1", aggregate.EquippedWeaponName);
119      }
120  
121      [Fact]
122      public void FromEvents_ShouldApplyPerkUnlockedCorrectly()
123      {
124          // Arrange
125          var operatorId = OperatorId.NewId();
126          var evt1 = new OperatorCreatedEvent(operatorId, "TestOperator");
127          var evt2 = new PerkUnlockedEvent(operatorId, 1, "Fast Reload", evt1.Hash);
128          var evt3 = new PerkUnlockedEvent(operatorId, 2, "Double Tap", evt2.Hash);
129          var events = new List<OperatorEvent> { evt1, evt2, evt3 };
130  
131          // Act
132          var aggregate = OperatorAggregate.FromEvents(events);
133  
134          // Assert
135          Assert.Equal(2, aggregate.UnlockedPerks.Count);
136          Assert.Contains("Fast Reload", aggregate.UnlockedPerks);
137          Assert.Contains("Double Tap", aggregate.UnlockedPerks);
138      }
139  
140      [Fact]
141      public void GetLastEventHash_ShouldReturnEmptyForNewAggregate()
142      {
143          // Arrange
144          var operatorId = OperatorId.NewId();
145          var evt = new OperatorCreatedEvent(operatorId, "TestOperator");
146          var aggregate = OperatorAggregate.Create(evt);
147  
148          // Act
149          var lastHash = aggregate.GetLastEventHash();
150  
151          // Assert
152          Assert.NotEmpty(lastHash);
153          Assert.Equal(evt.Hash, lastHash);
154      }
155  
156      [Fact]
157      public void GetLastEventHash_ShouldReturnLatestHash()
158      {
159          // Arrange
160          var operatorId = OperatorId.NewId();
161          var evt1 = new OperatorCreatedEvent(operatorId, "TestOperator");
162          var evt2 = new XpGainedEvent(operatorId, 1, 100, "Victory", evt1.Hash);
163          var events = new List<OperatorEvent> { evt1, evt2 };
164          var aggregate = OperatorAggregate.FromEvents(events);
165  
166          // Act
167          var lastHash = aggregate.GetLastEventHash();
168  
169          // Assert
170          Assert.Equal(evt2.Hash, lastHash);
171      }
172  
173      [Fact]
174      public void CreateCombatSnapshot_ShouldCopyBasicStats()
175      {
176          // Arrange
177          var operatorId = OperatorId.NewId();
178          var evt = new OperatorCreatedEvent(operatorId, "TestOperator");
179          var aggregate = OperatorAggregate.Create(evt);
180  
181          // Act
182          var snapshot = aggregate.CreateCombatSnapshot();
183  
184          // Assert
185          Assert.Equal(operatorId.Value, snapshot.Id);
186          Assert.Equal("TestOperator", snapshot.Name);
187          Assert.Equal(100f, snapshot.MaxHealth);
188          Assert.Equal(100f, snapshot.Health);
189      }
190  
191      [Fact]
192      public void WoundsTreated_ShouldNotExceedMaxHealth()
193      {
194          // Arrange
195          var operatorId = OperatorId.NewId();
196          var evt1 = new OperatorCreatedEvent(operatorId, "TestOperator");
197  
198          // Apply healing to an operator at full health
199          var evt2 = new WoundsTreatedEvent(operatorId, 1, 50f, evt1.Hash);
200  
201          // Act
202          var events = new List<OperatorEvent> { evt1, evt2 };
203          var restoredAggregate = OperatorAggregate.FromEvents(events);
204  
205          // Assert - Health should remain capped at max even with excess healing
206          Assert.Equal(100f, restoredAggregate.CurrentHealth);
207      }
208  
209      [Fact]
210      public void CombatVictory_ShouldNotIncrementStreak()
211      {
212          // CombatVictoryEvent clears the active combat session but does NOT increment ExfilStreak.
213          // ExfilStreak only increments when the infil is completed successfully (InfilEndedEvent with wasSuccessful=true).
214          
215          // Arrange
216          var operatorId = OperatorId.NewId();
217          var evt1 = new OperatorCreatedEvent(operatorId, "TestOperator");
218          var evt2 = new CombatVictoryEvent(operatorId, 1, evt1.Hash);
219          var evt3 = new CombatVictoryEvent(operatorId, 2, evt2.Hash);
220  
221          // Act
222          var events = new List<OperatorEvent> { evt1, evt2, evt3 };
223          var aggregate = OperatorAggregate.FromEvents(events);
224  
225          // Assert
226          Assert.Equal(0, aggregate.ExfilStreak);
227          Assert.False(aggregate.IsDead);
228      }
229  
230      [Fact]
231      public void ExfilFailed_ShouldResetStreak()
232      {
233          // Arrange
234          var operatorId = OperatorId.NewId();
235          var evt1 = new OperatorCreatedEvent(operatorId, "TestOperator");
236          var evt2 = new CombatVictoryEvent(operatorId, 1, evt1.Hash);
237          var evt3 = new CombatVictoryEvent(operatorId, 2, evt2.Hash);
238          var evt4 = new ExfilFailedEvent(operatorId, 3, "Retreat", evt3.Hash);
239  
240          // Act
241          var events = new List<OperatorEvent> { evt1, evt2, evt3, evt4 };
242          var aggregate = OperatorAggregate.FromEvents(events);
243  
244          // Assert
245          Assert.Equal(0, aggregate.ExfilStreak);
246          Assert.False(aggregate.IsDead);
247      }
248  
249      [Fact]
250      public void OperatorDied_ShouldRespawnWithFullHealthAndResetStreak()
251      {
252          // Arrange
253          var operatorId = OperatorId.NewId();
254          var evt1 = new OperatorCreatedEvent(operatorId, "TestOperator");
255          var evt2 = new CombatVictoryEvent(operatorId, 1, evt1.Hash);
256          var evt3 = new OperatorDiedEvent(operatorId, 2, "Combat casualty", evt2.Hash);
257  
258          // Act
259          var events = new List<OperatorEvent> { evt1, evt2, evt3 };
260          var aggregate = OperatorAggregate.FromEvents(events);
261  
262          // Assert
263          Assert.False(aggregate.IsDead); // Operator respawns after death
264          Assert.Equal(aggregate.MaxHealth, aggregate.CurrentHealth); // Health restored to full after death
265          Assert.Equal(0, aggregate.ExfilStreak);
266      }
267  
268      [Fact]
269      public void CombatVictory_ShouldClearActiveSessionId()
270      {
271          // Arrange
272          var operatorId = OperatorId.NewId();
273          var sessionId = Guid.NewGuid();
274          var evt1 = new OperatorCreatedEvent(operatorId, "TestOperator");
275          var evt2 = new InfilStartedEvent(operatorId, 1, sessionId, "AK-47", DateTimeOffset.UtcNow, evt1.Hash);
276          var evt3 = new CombatVictoryEvent(operatorId, 2, evt2.Hash);
277  
278          // Act
279          var events = new List<OperatorEvent> { evt1, evt2, evt3 };
280          var aggregate = OperatorAggregate.FromEvents(events);
281  
282          // Assert
283          Assert.Equal(OperatorMode.Infil, aggregate.CurrentMode); // Should stay in Infil mode
284          Assert.Equal(0, aggregate.ExfilStreak); // Streak NOT incremented (only increments on infil completion)
285          Assert.Null(aggregate.ActiveCombatSessionId); // ActiveSessionId cleared to prevent auto-resume of completed session
286      }
287  
288      [Fact]
289      public void FromEvents_ShouldRollbackOnHashFailure()
290      {
291          // Arrange
292          var operatorId = OperatorId.NewId();
293          var evt1 = new OperatorCreatedEvent(operatorId, "TestOperator");
294          var evt2 = new XpGainedEvent(operatorId, 1, 100, "Victory", evt1.Hash);
295          
296          // Create a corrupted event with wrong hash
297          var evt3 = new XpGainedEvent(operatorId, 2, 50, "Survived", evt2.Hash);
298          // Manually corrupt the event by creating a new one with same data but we'll simulate corruption
299          // by passing wrong previous hash to the next event
300          var evt4 = new XpGainedEvent(operatorId, 3, 25, "Bonus", "corrupted_hash");
301  
302          var events = new List<OperatorEvent> { evt1, evt2, evt3, evt4 };
303  
304          // Act - The aggregate should only replay valid events up to evt3
305          var aggregate = OperatorAggregate.FromEvents(events);
306  
307          // Assert - Should have rolled back to last valid event (evt3)
308          Assert.Equal(150, aggregate.TotalXp); // Only evt2 and evt3 applied
309          Assert.Equal(2, aggregate.CurrentSequence);
310      }
311  
312      [Fact]
313      public void FromEvents_ShouldThrowIfFirstEventInvalid()
314      {
315          // Arrange
316          var operatorId = OperatorId.NewId();
317          // Create event with wrong previous hash for genesis event
318          var evt1 = new XpGainedEvent(operatorId, 0, 100, "Invalid", "should_be_empty");
319  
320          var events = new List<OperatorEvent> { evt1 };
321  
322          // Act & Assert
323          var ex = Assert.Throws<InvalidOperationException>(() => OperatorAggregate.FromEvents(events));
324          Assert.Contains("No valid events", ex.Message);
325      }
326  
327      [Fact]
328      public void NewOperator_ShouldStartWithZeroStreakAndAlive()
329      {
330          // Arrange
331          var operatorId = OperatorId.NewId();
332          var evt = new OperatorCreatedEvent(operatorId, "TestOperator");
333  
334          // Act
335          var aggregate = OperatorAggregate.Create(evt);
336  
337          // Assert
338          Assert.Equal(0, aggregate.ExfilStreak);
339          Assert.False(aggregate.IsDead);
340      }
341  }