using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using NeonSprawl.Server.Game.Contracts; using NeonSprawl.Server.Game.PositionState; using NeonSprawl.Server.Tests; using Xunit; 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 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); } }