using System.Net; using System.Net.Http.Json; using NeonSprawl.Server.Game.AbilityInput; using NeonSprawl.Server.Game.Targeting; using NeonSprawl.Server.Tests; using Xunit; namespace NeonSprawl.Server.Tests.Game.AbilityInput; public sealed class CooldownSnapshotApiTests { private static async Task BindSlot0PulseAsync(HttpClient client) { var post = await client.PostAsJsonAsync( "/game/players/dev-local-1/hotbar-loadout", new HotbarLoadoutUpdateRequest { SchemaVersion = HotbarLoadoutUpdateRequest.CurrentSchemaVersion, Slots = [new HotbarSlotBindingJson { SlotIndex = 0, AbilityId = PrototypeAbilityRegistry.PrototypePulse }], }); post.EnsureSuccessStatusCode(); } private static async Task LockPrototypeTargetAlphaAsync(HttpClient client) { var response = await client.PostAsJsonAsync( "/game/players/dev-local-1/target/select", new TargetSelectRequest { SchemaVersion = TargetSelectRequest.CurrentSchemaVersion, TargetId = PrototypeTargetRegistry.PrototypeTargetAlphaId, }); response.EnsureSuccessStatusCode(); } [Fact] public async Task GetCooldownSnapshot_ShouldReturnNotFound_WhenPlayerUnknown() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var response = await client.GetAsync("/game/players/missing-player/cooldown-snapshot"); // Assert Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } [Fact] public async Task GetCooldownSnapshot_ShouldReturnV1Schema_WithEightSlots_WhenPlayerKnown() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); // Act var response = await client.GetAsync("/game/players/dev-local-1/cooldown-snapshot"); // Assert var body = await response.Content.ReadFromJsonAsync(); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.NotNull(body); Assert.Equal(CooldownSnapshotResponse.CurrentSchemaVersion, body!.SchemaVersion); Assert.Equal("dev-local-1", body.PlayerId); Assert.Equal(HotbarLoadoutResponse.SlotCountV1, body.Slots.Count); Assert.All(body.Slots, s => Assert.Null(s.CooldownEndsAtUtc)); } [Fact] public async Task GetCooldownSnapshot_ShouldExposeSlotEnd_WhenCastAccepted_ThenClear_WhenAdvancedPastDuration() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); Assert.NotNull(factory.FakeClock); await BindSlot0PulseAsync(client); await LockPrototypeTargetAlphaAsync(client); var cast = new AbilityCastRequest { SchemaVersion = AbilityCastRequest.CurrentSchemaVersion, SlotIndex = 0, AbilityId = PrototypeAbilityRegistry.PrototypePulse, TargetId = PrototypeTargetRegistry.PrototypeTargetAlphaId, }; // Act var castResponse = await client.PostAsJsonAsync("/game/players/dev-local-1/ability-cast", cast); var snapDuring = await client.GetAsync("/game/players/dev-local-1/cooldown-snapshot"); factory.FakeClock!.Advance(AbilityPrototypeCooldown.GlobalDuration + TimeSpan.FromMilliseconds(50)); var snapAfter = await client.GetAsync("/game/players/dev-local-1/cooldown-snapshot"); // Assert castResponse.EnsureSuccessStatusCode(); var during = await snapDuring.Content.ReadFromJsonAsync(); var after = await snapAfter.Content.ReadFromJsonAsync(); Assert.NotNull(during); Assert.NotNull(after); var slot0During = during!.Slots.Single(s => s.SlotIndex == 0); Assert.NotNull(slot0During.CooldownEndsAtUtc); Assert.True(slot0During.CooldownEndsAtUtc > during.ServerTimeUtc); var slot0After = after!.Slots.Single(s => s.SlotIndex == 0); Assert.Null(slot0After.CooldownEndsAtUtc); } }