using NeonSprawl.Server.Game.Contracts; using NeonSprawl.Server.Game.PositionState; namespace NeonSprawl.Server.Tests.Game.Contracts; public sealed class InMemoryContractInstanceStoreTests { private const string PlayerId = "dev-local-1"; private const string UnknownPlayerId = "unknown-player-xyz"; private const string InstanceId = "prototype_contract_instance_001"; private const string SecondInstanceId = "prototype_contract_instance_002"; private const string TemplateId = "prototype_contract_clear_combat_pocket"; private const string SeedBucket = "2026-06-22"; private static readonly DateTimeOffset IssuedAt = new(2026, 6, 22, 10, 0, 0, TimeSpan.Zero); private static readonly DateTimeOffset CompletedAt = new(2026, 6, 22, 12, 0, 0, TimeSpan.Zero); [Fact] public void TryGet_ShouldReturnFalse_WhenRowMissing() { // Arrange var store = CreateStore(); // Act var found = store.TryGet(PlayerId, InstanceId, out _); // Assert Assert.False(found); } [Fact] public void TryCreateActive_ShouldPersistAllFields() { // Arrange var store = CreateStore(); // Act var created = store.TryCreateActive( PlayerId, InstanceId, TemplateId, SeedBucket, IssuedAt, out var snapshot); // Assert Assert.True(created); Assert.Equal(InstanceId, snapshot.ContractInstanceId); Assert.Equal(TemplateId, snapshot.TemplateId); Assert.Equal(PlayerId, snapshot.PlayerId); Assert.Equal(ContractInstanceStatus.Active, snapshot.Status); Assert.Equal(SeedBucket, snapshot.SeedBucket); Assert.Equal(IssuedAt, snapshot.IssuedAt); Assert.Null(snapshot.CompletedAt); } [Fact] public void TryCreateActive_ShouldDenyDuplicateInstanceId() { // Arrange var store = CreateStore(); Assert.True(store.TryCreateActive( PlayerId, InstanceId, TemplateId, SeedBucket, IssuedAt, out var firstSnapshot)); // Act var duplicate = store.TryCreateActive( PlayerId, InstanceId, TemplateId, SeedBucket, IssuedAt.AddHours(1), out var secondSnapshot); // Assert Assert.False(duplicate); Assert.Equal(firstSnapshot.ContractInstanceId, secondSnapshot.ContractInstanceId); Assert.Equal(firstSnapshot.IssuedAt, secondSnapshot.IssuedAt); Assert.Equal(ContractInstanceStatus.Active, secondSnapshot.Status); } [Fact] public void TryCreateActive_ShouldDenySecondActiveForSamePlayer() { // Arrange var store = CreateStore(); Assert.True(store.TryCreateActive( PlayerId, InstanceId, TemplateId, SeedBucket, IssuedAt, out _)); // Act var second = store.TryCreateActive( PlayerId, SecondInstanceId, TemplateId, SeedBucket, IssuedAt.AddHours(1), out var snapshot); // Assert Assert.False(second); Assert.Equal(InstanceId, snapshot.ContractInstanceId); Assert.Equal(ContractInstanceStatus.Active, snapshot.Status); Assert.True(store.TryGetActiveForPlayer(PlayerId, out var active)); Assert.Equal(InstanceId, active.ContractInstanceId); } [Fact] public void TryMarkComplete_ShouldSetCompletedStatusAndTimestamp() { // Arrange var store = CreateStore(); Assert.True(store.TryCreateActive( PlayerId, InstanceId, TemplateId, SeedBucket, IssuedAt, out _)); // Act var completed = store.TryMarkComplete(PlayerId, InstanceId, CompletedAt, out var snapshot); // Assert Assert.True(completed); Assert.Equal(ContractInstanceStatus.Completed, snapshot.Status); Assert.Equal(CompletedAt, snapshot.CompletedAt); Assert.False(store.TryGetActiveForPlayer(PlayerId, out _)); } [Fact] public void TryMarkComplete_ShouldBeIdempotent() { // Arrange var store = CreateStore(); Assert.True(store.TryCreateActive( PlayerId, InstanceId, TemplateId, SeedBucket, IssuedAt, out _)); // Act var first = store.TryMarkComplete(PlayerId, InstanceId, CompletedAt, out var firstSnapshot); var second = store.TryMarkComplete( PlayerId, InstanceId, CompletedAt.AddHours(1), out var secondSnapshot); // Assert Assert.True(first); Assert.False(second); Assert.Equal(ContractInstanceStatus.Completed, secondSnapshot.Status); Assert.Equal(CompletedAt, secondSnapshot.CompletedAt); Assert.Equal(firstSnapshot.TemplateId, secondSnapshot.TemplateId); } [Fact] public void TryCreateActive_ShouldAllowNewInstanceAfterComplete() { // Arrange var store = CreateStore(); Assert.True(store.TryCreateActive( PlayerId, InstanceId, TemplateId, SeedBucket, IssuedAt, out _)); Assert.True(store.TryMarkComplete(PlayerId, InstanceId, CompletedAt, out _)); // Act var reissued = store.TryCreateActive( PlayerId, SecondInstanceId, TemplateId, SeedBucket, IssuedAt.AddDays(1), out var snapshot); // Assert Assert.True(reissued); Assert.Equal(SecondInstanceId, snapshot.ContractInstanceId); Assert.Equal(ContractInstanceStatus.Active, snapshot.Status); } [Fact] public void TryCreateActive_ShouldReturnFalse_ForUnknownPlayer() { // Arrange var store = CreateStore(); // Act var created = store.TryCreateActive( UnknownPlayerId, InstanceId, TemplateId, SeedBucket, IssuedAt, out _); // Assert Assert.False(created); } [Theory] [InlineData("", InstanceId, TemplateId, SeedBucket)] [InlineData(" ", InstanceId, TemplateId, SeedBucket)] [InlineData(PlayerId, "", TemplateId, SeedBucket)] [InlineData(PlayerId, " ", TemplateId, SeedBucket)] [InlineData(PlayerId, InstanceId, "", SeedBucket)] [InlineData(PlayerId, InstanceId, TemplateId, "")] public void TryCreateActive_ShouldReturnFalse_WhenRequiredIdsEmpty( string playerId, string instanceId, string templateId, string seedBucket) { // Arrange var store = CreateStore(); // Act var created = store.TryCreateActive( playerId, instanceId, templateId, seedBucket, IssuedAt, out _); // Assert Assert.False(created); } [Fact] public async Task TryCreateActiveAndTryMarkComplete_ShouldKeepActiveIndexConsistent_WhenRacingOnSameInstanceId() { // Arrange var store = CreateStore(); // Act await Task.WhenAll( Enumerable.Range(0, 32).Select(i => Task.Run(() => { if (i % 2 == 0) { _ = store.TryCreateActive( PlayerId, InstanceId, TemplateId, SeedBucket, IssuedAt, out _); return; } _ = store.TryMarkComplete(PlayerId, InstanceId, CompletedAt, out _); }))); // Assert if (store.TryGetActiveForPlayer(PlayerId, out var active)) { Assert.True(store.TryGet(PlayerId, active.ContractInstanceId, out var row)); Assert.Equal(ContractInstanceStatus.Active, row.Status); } if (store.TryGet(PlayerId, InstanceId, out var final) && final.Status == ContractInstanceStatus.Completed) { Assert.False(store.TryGetActiveForPlayer(PlayerId, out _)); } } [Fact] public async Task TryGetActiveForPlayer_ShouldKeepActiveIndexConsistent_WhenRacingWithCompleteAndReissue() { // Arrange var store = CreateStore(); Assert.True(store.TryCreateActive( PlayerId, InstanceId, TemplateId, SeedBucket, IssuedAt, out _)); // Act await Task.WhenAll( Enumerable.Range(0, 32).Select(i => Task.Run(() => { if (i % 3 == 0) { _ = store.TryGetActiveForPlayer(PlayerId, out _); return; } if (i % 3 == 1) { _ = store.TryMarkComplete(PlayerId, InstanceId, CompletedAt, out _); return; } _ = store.TryCreateActive( PlayerId, SecondInstanceId, TemplateId, SeedBucket, IssuedAt.AddDays(1), out _); }))); // Assert if (store.TryGetActiveForPlayer(PlayerId, out var active)) { Assert.True(store.TryGet(PlayerId, active.ContractInstanceId, out var row)); Assert.Equal(ContractInstanceStatus.Active, row.Status); } Assert.False( store.TryGet(PlayerId, InstanceId, out var first) && store.TryGet(PlayerId, SecondInstanceId, out var second) && first.Status == ContractInstanceStatus.Active && second.Status == ContractInstanceStatus.Active); } [Fact] public void ListForPlayer_ShouldReturnEmpty_WhenPlayerHasNoInstances() { // Arrange var store = CreateStore(); // Act var rows = store.ListForPlayer(PlayerId, maxCompletedRows: 10); // Assert Assert.Empty(rows); } [Fact] public void ListForPlayer_ShouldReturnEmpty_ForUnknownPlayer() { // Arrange var store = CreateStore(); // Act var rows = store.ListForPlayer(UnknownPlayerId, maxCompletedRows: 10); // Assert Assert.Empty(rows); } [Fact] public void ListForPlayer_ShouldReturnActiveOnly_WhenNoCompletedRows() { // Arrange var store = CreateStore(); Assert.True(store.TryCreateActive( PlayerId, InstanceId, TemplateId, SeedBucket, IssuedAt, out _)); // Act var rows = store.ListForPlayer(PlayerId, maxCompletedRows: 10); // Assert var row = Assert.Single(rows); Assert.Equal(InstanceId, row.ContractInstanceId); Assert.Equal(ContractInstanceStatus.Active, row.Status); } [Fact] public void ListForPlayer_ShouldReturnCompletedOrderedByIssuedAt_WhenNoActive() { // Arrange var store = CreateStore(); Assert.True(store.TryCreateActive( PlayerId, InstanceId, TemplateId, SeedBucket, IssuedAt, out _)); Assert.True(store.TryMarkComplete(PlayerId, InstanceId, CompletedAt, out _)); Assert.True(store.TryCreateActive( PlayerId, SecondInstanceId, TemplateId, SeedBucket, IssuedAt.AddDays(1), out _)); Assert.True(store.TryMarkComplete( PlayerId, SecondInstanceId, CompletedAt.AddDays(1), out _)); // Act var rows = store.ListForPlayer(PlayerId, maxCompletedRows: 10); // Assert Assert.Equal(2, rows.Count); Assert.Equal(SecondInstanceId, rows[0].ContractInstanceId); Assert.Equal(InstanceId, rows[1].ContractInstanceId); } [Fact] public void ListForPlayer_ShouldPrependActiveAndCapCompletedRows() { // Arrange var store = CreateStore(); for (var i = 0; i < 12; i++) { var instanceId = $"prototype_contract_instance_done_{i:D2}"; Assert.True(store.TryCreateActive( PlayerId, instanceId, TemplateId, SeedBucket, IssuedAt.AddHours(i), out _)); Assert.True(store.TryMarkComplete( PlayerId, instanceId, CompletedAt.AddHours(i), out _)); } const string activeId = "prototype_contract_instance_active"; Assert.True(store.TryCreateActive( PlayerId, activeId, TemplateId, SeedBucket, IssuedAt.AddDays(3), out _)); // Act var rows = store.ListForPlayer(PlayerId, maxCompletedRows: 10); // Assert Assert.Equal(11, rows.Count); Assert.Equal(activeId, rows[0].ContractInstanceId); Assert.Equal(ContractInstanceStatus.Active, rows[0].Status); Assert.All(rows.Skip(1), row => Assert.Equal(ContractInstanceStatus.Completed, row.Status)); Assert.DoesNotContain( rows.Skip(1), row => string.Equals(row.ContractInstanceId, "prototype_contract_instance_done_00", StringComparison.Ordinal)); Assert.DoesNotContain( rows.Skip(1), row => string.Equals(row.ContractInstanceId, "prototype_contract_instance_done_01", StringComparison.Ordinal)); } [Fact] public void TryClearInstance_ShouldReturnTrue_WhenRowMissing() { // Arrange var store = CreateStore(); // Act var cleared = store.TryClearInstance(PlayerId, InstanceId); // Assert Assert.True(cleared); Assert.False(store.TryGet(PlayerId, InstanceId, out _)); } [Fact] public async Task Host_ShouldResolveContractInstanceStoresFromDi() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); using var scope = factory.Services.CreateScope(); var instanceStore = scope.ServiceProvider.GetRequiredService(); var outcomeStore = scope.ServiceProvider.GetRequiredService(); // Act var created = instanceStore.TryCreateActive( PlayerId, InstanceId, TemplateId, SeedBucket, IssuedAt, out var snapshot); // Assert Assert.IsType(instanceStore); Assert.IsType(outcomeStore); Assert.True(created); Assert.Equal(TemplateId, snapshot.TemplateId); } private static InMemoryContractInstanceStore CreateStore() { var options = Options.Create(new GamePositionOptions { DevPlayerId = PlayerId }); return new InMemoryContractInstanceStore(options); } }