using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using NeonSprawl.Server.Game.Factions; using NeonSprawl.Server.Game.PositionState; using Xunit; namespace NeonSprawl.Server.Tests.Game.Factions; public sealed class InMemoryFactionStandingStoreTests { private const string PlayerId = "dev-local-1"; private const string UnknownPlayerId = "unknown-player-xyz"; private const string GridFactionId = "prototype_faction_grid_operators"; private const string RustFactionId = "prototype_faction_rust_collective"; [Fact] public void TryGetStanding_ShouldReturnZero_WhenRowMissing() { // Arrange var store = CreateStore(); // Act var outcome = store.TryGetStanding(PlayerId, GridFactionId); // Assert Assert.True(outcome.Success); Assert.Null(outcome.ReasonCode); Assert.Equal(0, outcome.Standing); } [Fact] public void TryApplyStandingDelta_ShouldReturnFifteen_WhenFirstApplyFromZero() { // Arrange var store = CreateStore(); // Act var outcome = store.TryApplyStandingDelta(PlayerId, GridFactionId, 15); // Assert Assert.True(outcome.Success); Assert.Equal(0, outcome.PreviousStanding); Assert.Equal(15, outcome.NewStanding); var readBack = store.TryGetStanding(PlayerId, GridFactionId); Assert.True(readBack.Success); Assert.Equal(15, readBack.Standing); } [Fact] public void TryApplyStandingDelta_ShouldClampToMax_WhenDeltaExceedsBand() { // Arrange var store = CreateStore(); Assert.True(store.TryApplyStandingDelta(PlayerId, GridFactionId, 95).Success); // Act var outcome = store.TryApplyStandingDelta(PlayerId, GridFactionId, 50); // Assert Assert.True(outcome.Success); Assert.Equal(95, outcome.PreviousStanding); Assert.Equal(100, outcome.NewStanding); Assert.Equal(100, store.TryGetStanding(PlayerId, GridFactionId).Standing); } [Fact] public void TryApplyStandingDelta_ShouldClampToMin_WhenDeltaBelowBand() { // Arrange var store = CreateStore(); Assert.True(store.TryApplyStandingDelta(PlayerId, GridFactionId, -95).Success); // Act var outcome = store.TryApplyStandingDelta(PlayerId, GridFactionId, -50); // Assert Assert.True(outcome.Success); Assert.Equal(-95, outcome.PreviousStanding); Assert.Equal(-100, outcome.NewStanding); Assert.Equal(-100, store.TryGetStanding(PlayerId, GridFactionId).Standing); } [Fact] public void TryGetStanding_ShouldClampRead_WhenRawStoredValueExceedsMax() { // Arrange var store = CreateStore(); store.SeedRawStandingForTests(PlayerId, GridFactionId, 999); // Act var outcome = store.TryGetStanding(PlayerId, GridFactionId); // Assert Assert.True(outcome.Success); Assert.Null(outcome.ReasonCode); Assert.Equal(100, outcome.Standing); } [Fact] public void TryGetStanding_ShouldReturnUnknownFaction_WhenFactionIdEmpty() { // Arrange var store = CreateStore(); // Act var read = store.TryGetStanding(PlayerId, " "); var write = store.TryApplyStandingDelta(PlayerId, "", 5); // Assert Assert.False(read.Success); Assert.Equal(FactionStandingReasonCodes.UnknownFaction, read.ReasonCode); Assert.False(write.Success); Assert.Equal(FactionStandingReasonCodes.UnknownFaction, write.ReasonCode); } [Fact] public void TryGetStanding_ShouldDenyUnknownFaction_WithStructuredReason() { // Arrange var store = CreateStore(); // Act var read = store.TryGetStanding(PlayerId, "not_a_real_faction"); var write = store.TryApplyStandingDelta(PlayerId, "not_a_real_faction", 5); // Assert Assert.False(read.Success); Assert.Equal(FactionStandingReasonCodes.UnknownFaction, read.ReasonCode); Assert.Equal(0, read.Standing); Assert.False(write.Success); Assert.Equal(FactionStandingReasonCodes.UnknownFaction, write.ReasonCode); Assert.Equal(0, store.TryGetStanding(PlayerId, GridFactionId).Standing); } [Fact] public void TryApplyStandingDelta_ShouldDenyNonWritablePlayer() { // Arrange var store = CreateStore(); // Act var outcome = store.TryApplyStandingDelta(UnknownPlayerId, GridFactionId, 10); // Assert Assert.False(outcome.Success); Assert.Equal(FactionStandingReasonCodes.PlayerNotWritable, outcome.ReasonCode); } [Fact] public void TryGetStanding_ShouldDenyNonWritablePlayer() { // Arrange var store = CreateStore(); // Act var outcome = store.TryGetStanding(UnknownPlayerId, GridFactionId); // Assert Assert.False(outcome.Success); Assert.Equal(FactionStandingReasonCodes.PlayerNotWritable, outcome.ReasonCode); } [Fact] public void TryApplyStandingDelta_ShouldTrackSeparateFactionsPerPlayer() { // Arrange var store = CreateStore(); // Act Assert.True(store.TryApplyStandingDelta(PlayerId, GridFactionId, 15).Success); Assert.True(store.TryApplyStandingDelta(PlayerId, RustFactionId, 10).Success); // Assert Assert.Equal(15, store.TryGetStanding(PlayerId, GridFactionId).Standing); Assert.Equal(10, store.TryGetStanding(PlayerId, RustFactionId).Standing); } [Fact] public async Task Host_ShouldResolveFactionStandingStoresFromDi_WhenStartupSucceeds() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); using var client = factory.CreateClient(); _ = await client.GetAsync("/health"); // Act var standingStore = factory.Services.GetRequiredService(); var auditStore = factory.Services.GetRequiredService(); var read = standingStore.TryGetStanding(PlayerId, GridFactionId); // Assert Assert.NotNull(auditStore); Assert.True(read.Success); Assert.Equal(0, read.Standing); } private static InMemoryFactionStandingStore CreateStore() { var registry = CreatePrototypeRegistry(); var options = Options.Create(new GamePositionOptions { DevPlayerId = PlayerId }); return new InMemoryFactionStandingStore(options, registry); } private static FactionDefinitionRegistry CreatePrototypeRegistry() { var byId = new Dictionary(StringComparer.Ordinal) { [GridFactionId] = PrototypeE7M3FactionCatalogRules.ExpectedFactionFreeze[GridFactionId], [RustFactionId] = PrototypeE7M3FactionCatalogRules.ExpectedFactionFreeze[RustFactionId], }; var catalog = new FactionDefinitionCatalog("/tmp/catalog", byId, catalogJsonFileCount: 1); return new FactionDefinitionRegistry(catalog); } }