using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using NeonSprawl.Server.Game.Factions; using NeonSprawl.Server.Game.PositionState; using NeonSprawl.Server.Tests; using Xunit; namespace NeonSprawl.Server.Tests.Game.Factions; public sealed class ReputationOperationsTests { 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"; private const string QuestSourceId = "prototype_quest_operator_chain"; private static readonly DateTimeOffset RecordedAt = new(2026, 6, 15, 14, 0, 0, TimeSpan.Zero); [Fact] public void TryApplyDelta_ShouldApplyStandingAndAppendAudit_WhenHappyPath() { // Arrange var standingStore = CreateStandingStore(); var auditStore = CreateAuditStore(); var timeProvider = new FakeTimeProvider(RecordedAt); // Act var outcome = ReputationOperations.TryApplyDelta( PlayerId, GridFactionId, 15, "delta-001", ReputationDeltaSourceKinds.QuestCompletion, QuestSourceId, standingStore, auditStore, timeProvider); // Assert Assert.True(outcome.Success); Assert.Null(outcome.ReasonCode); Assert.Equal(0, outcome.PreviousStanding); Assert.Equal(15, outcome.NewStanding); Assert.NotNull(outcome.AuditRow); Assert.Equal("delta-001", outcome.AuditRow!.Id); Assert.Equal(15, outcome.AuditRow.DeltaAmount); Assert.Equal(15, outcome.AuditRow.ResultingStanding); Assert.Equal(ReputationDeltaSourceKinds.QuestCompletion, outcome.AuditRow.SourceKind); Assert.Equal(QuestSourceId, outcome.AuditRow.SourceId); Assert.Equal(RecordedAt, outcome.AuditRow.RecordedAt); var rows = auditStore.GetDeltasForPlayer(PlayerId); var read = Assert.Single(rows); Assert.Equal(outcome.AuditRow, read); Assert.Equal(15, standingStore.TryGetStanding(PlayerId, GridFactionId).Standing); } [Fact] public void TryApplyDelta_ShouldClampToMax_WhenDeltaExceedsBand() { // Arrange var standingStore = CreateStandingStore(); var auditStore = CreateAuditStore(); Assert.True(standingStore.TryApplyStandingDelta(PlayerId, GridFactionId, 95).Success); // Act var outcome = ReputationOperations.TryApplyDelta( PlayerId, GridFactionId, 50, "delta-max", ReputationDeltaSourceKinds.QuestCompletion, QuestSourceId, standingStore, auditStore, TimeProvider.System); // Assert Assert.True(outcome.Success); Assert.Equal(95, outcome.PreviousStanding); Assert.Equal(100, outcome.NewStanding); Assert.Equal(5, outcome.AuditRow!.DeltaAmount); Assert.Equal(100, outcome.AuditRow.ResultingStanding); Assert.Equal(100, standingStore.TryGetStanding(PlayerId, GridFactionId).Standing); } [Fact] public void TryApplyDelta_ShouldClampToMin_WhenDeltaBelowBand() { // Arrange var standingStore = CreateStandingStore(); var auditStore = CreateAuditStore(); Assert.True(standingStore.TryApplyStandingDelta(PlayerId, GridFactionId, -95).Success); // Act var outcome = ReputationOperations.TryApplyDelta( PlayerId, GridFactionId, -50, "delta-min", ReputationDeltaSourceKinds.QuestCompletion, QuestSourceId, standingStore, auditStore, TimeProvider.System); // Assert Assert.True(outcome.Success); Assert.Equal(-95, outcome.PreviousStanding); Assert.Equal(-100, outcome.NewStanding); Assert.Equal(-5, outcome.AuditRow!.DeltaAmount); Assert.Equal(-100, outcome.AuditRow.ResultingStanding); Assert.Equal(-100, standingStore.TryGetStanding(PlayerId, GridFactionId).Standing); } [Fact] public void TryApplyDelta_ShouldDenyUnknownFaction_WithStructuredReason() { // Arrange var standingStore = CreateStandingStore(); var auditStore = CreateAuditStore(); // Act var outcome = ReputationOperations.TryApplyDelta( PlayerId, "not_a_real_faction", 10, "delta-unknown", ReputationDeltaSourceKinds.QuestCompletion, QuestSourceId, standingStore, auditStore, TimeProvider.System); // Assert Assert.False(outcome.Success); Assert.Equal(FactionStandingReasonCodes.UnknownFaction, outcome.ReasonCode); Assert.Equal(0, standingStore.TryGetStanding(PlayerId, GridFactionId).Standing); Assert.Empty(auditStore.GetDeltasForPlayer(PlayerId)); } [Fact] public void TryApplyDelta_ShouldDenyInvalidDelta_WhenAmountZero() { // Arrange var standingStore = CreateStandingStore(); var auditStore = CreateAuditStore(); // Act var outcome = ReputationOperations.TryApplyDelta( PlayerId, GridFactionId, 0, "delta-zero", ReputationDeltaSourceKinds.QuestCompletion, QuestSourceId, standingStore, auditStore, TimeProvider.System); // Assert Assert.False(outcome.Success); Assert.Equal(ReputationApplyReasonCodes.InvalidDelta, outcome.ReasonCode); Assert.Equal(0, standingStore.TryGetStanding(PlayerId, GridFactionId).Standing); Assert.Empty(auditStore.GetDeltasForPlayer(PlayerId)); } [Fact] public void TryApplyDelta_ShouldDenyInvalidDeltaId_WhenEmpty() { // Arrange var standingStore = CreateStandingStore(); var auditStore = CreateAuditStore(); // Act var outcome = ReputationOperations.TryApplyDelta( PlayerId, GridFactionId, 10, " ", ReputationDeltaSourceKinds.QuestCompletion, QuestSourceId, standingStore, auditStore, TimeProvider.System); // Assert Assert.False(outcome.Success); Assert.Equal(ReputationApplyReasonCodes.InvalidDeltaId, outcome.ReasonCode); Assert.Equal(0, standingStore.TryGetStanding(PlayerId, GridFactionId).Standing); Assert.Empty(auditStore.GetDeltasForPlayer(PlayerId)); } [Fact] public void TryApplyDelta_ShouldDenyInvalidSource_WhenEmpty() { // Arrange var standingStore = CreateStandingStore(); var auditStore = CreateAuditStore(); // Act var emptyKind = ReputationOperations.TryApplyDelta( PlayerId, GridFactionId, 10, "delta-source-kind", " ", QuestSourceId, standingStore, auditStore, TimeProvider.System); var emptyId = ReputationOperations.TryApplyDelta( PlayerId, RustFactionId, 10, "delta-source-id", ReputationDeltaSourceKinds.QuestCompletion, "", standingStore, auditStore, TimeProvider.System); // Assert Assert.False(emptyKind.Success); Assert.Equal(ReputationApplyReasonCodes.InvalidSource, emptyKind.ReasonCode); Assert.False(emptyId.Success); Assert.Equal(ReputationApplyReasonCodes.InvalidSource, emptyId.ReasonCode); Assert.Equal(0, standingStore.TryGetStanding(PlayerId, GridFactionId).Standing); Assert.Equal(0, standingStore.TryGetStanding(PlayerId, RustFactionId).Standing); Assert.Empty(auditStore.GetDeltasForPlayer(PlayerId)); } [Fact] public void TryApplyDelta_ShouldRollbackStanding_WhenAuditAppendFails() { // Arrange var standingStore = CreateStandingStore(); var auditStore = CreateAuditStore(); Assert.True(auditStore.TryAppend(CreateAuditRow("delta-conflict", 1, 1))); // Act var outcome = ReputationOperations.TryApplyDelta( PlayerId, GridFactionId, 15, "delta-conflict", ReputationDeltaSourceKinds.QuestCompletion, QuestSourceId, standingStore, auditStore, TimeProvider.System); // Assert Assert.False(outcome.Success); Assert.Equal(ReputationApplyReasonCodes.AuditAppendFailed, outcome.ReasonCode); Assert.Equal(0, outcome.PreviousStanding); Assert.Equal(0, outcome.NewStanding); Assert.Null(outcome.AuditRow); Assert.Equal(0, standingStore.TryGetStanding(PlayerId, GridFactionId).Standing); Assert.Single(auditStore.GetDeltasForPlayer(PlayerId)); } [Fact] public void TryApplyDelta_ShouldRestoreClampedStanding_WhenAuditAppendFails() { // Arrange var standingStore = CreateStandingStore(); var auditStore = CreateAuditStore(); Assert.True(standingStore.TryApplyStandingDelta(PlayerId, GridFactionId, 95).Success); Assert.True(auditStore.TryAppend(CreateAuditRow("delta-clamp-conflict", 1, 1))); // Act var outcome = ReputationOperations.TryApplyDelta( PlayerId, GridFactionId, 50, "delta-clamp-conflict", ReputationDeltaSourceKinds.QuestCompletion, QuestSourceId, standingStore, auditStore, TimeProvider.System); // Assert Assert.False(outcome.Success); Assert.Equal(ReputationApplyReasonCodes.AuditAppendFailed, outcome.ReasonCode); Assert.Equal(95, outcome.PreviousStanding); Assert.Equal(95, outcome.NewStanding); Assert.Null(outcome.AuditRow); Assert.Equal(95, standingStore.TryGetStanding(PlayerId, GridFactionId).Standing); Assert.Single(auditStore.GetDeltasForPlayer(PlayerId)); } [Fact] public void TryApplyDelta_ShouldDenyNonWritablePlayer() { // Arrange var standingStore = CreateStandingStore(); var auditStore = CreateAuditStore(); // Act var outcome = ReputationOperations.TryApplyDelta( UnknownPlayerId, GridFactionId, 10, "delta-player", ReputationDeltaSourceKinds.QuestCompletion, QuestSourceId, standingStore, auditStore, TimeProvider.System); // Assert Assert.False(outcome.Success); Assert.Equal(FactionStandingReasonCodes.PlayerNotWritable, outcome.ReasonCode); Assert.Empty(auditStore.GetDeltasForPlayer(UnknownPlayerId)); } [Fact] public async Task Host_ShouldApplyReputationDeltaViaOps_WhenStartupSucceeds() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); using var client = factory.CreateClient(); _ = await client.GetAsync("/health"); var standingStore = factory.Services.GetRequiredService(); var auditStore = factory.Services.GetRequiredService(); // Act var outcome = ReputationOperations.TryApplyDelta( PlayerId, GridFactionId, 15, "delta-host", ReputationDeltaSourceKinds.QuestCompletion, QuestSourceId, standingStore, auditStore, TimeProvider.System); // Assert Assert.True(outcome.Success); Assert.Equal(15, standingStore.TryGetStanding(PlayerId, GridFactionId).Standing); Assert.Single(auditStore.GetDeltasForPlayer(PlayerId)); } private static InMemoryFactionStandingStore CreateStandingStore() { var registry = CreatePrototypeRegistry(); var options = Options.Create(new GamePositionOptions { DevPlayerId = PlayerId }); return new InMemoryFactionStandingStore(options, registry); } private static InMemoryReputationDeltaStore CreateAuditStore() => new(); private static ReputationDeltaRow CreateAuditRow(string id, int delta, int resulting) => new( id, PlayerId, GridFactionId, delta, resulting, ReputationDeltaSourceKinds.QuestCompletion, QuestSourceId, RecordedAt); 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); } private sealed class FakeTimeProvider(DateTimeOffset utcNow) : TimeProvider { public override DateTimeOffset GetUtcNow() => utcNow; } }