using System.Net;
using System.Net.Http.Json;
using System.Text;
using NeonSprawl.Server.Game.AbilityInput;
using NeonSprawl.Server.Game.PositionState;
using NeonSprawl.Server.Game.Targeting;
using Xunit;

namespace NeonSprawl.Server.Tests.Game.AbilityInput;

/// <summary>HTTP integration tests for <see cref="AbilityCastApi"/>.</summary>
/// <remarks>
/// NEO-30: every JSON deny (<see cref="AbilityCastResponse.Accepted"/> <c>false</c>) asserts a non-whitespace
/// <see cref="AbilityCastResponse.ReasonCode"/> — matches Slice 3 <c>ability_cast_denied</c> payload expectations;
/// authoritative hook-site comments live on <see cref="AbilityCastApi"/> (cast route: <c>MapAbilityCastApi</c> extension in the same source file).
/// Optional dev: <c>NEON_SPRAWL_API_LOG=1</c> enables one-line <c>ability_cast_*</c> stdout per JSON cast response (see <see cref="Diagnostics.PrototypeApiConsoleLog"/>).
/// </remarks>
public sealed class AbilityCastApiTests
{
    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();
        var body = await response.Content.ReadFromJsonAsync<TargetSelectResponse>();
        Assert.NotNull(body);
        Assert.True(body!.SelectionApplied);
    }

    private static async Task TeleportDevPlayerAsync(HttpClient client, double x, double y, double z)
    {
        var response = await client.PostAsJsonAsync(
            "/game/players/dev-local-1/move-stream",
            new MoveStreamRequest
            {
                SchemaVersion = MoveStreamRequest.CurrentSchemaVersion,
                Targets = [new PositionVector { X = x, Y = y, Z = z }],
            });
        response.EnsureSuccessStatusCode();
    }

    [Fact]
    public async Task PostAbilityCast_ShouldAccept_WhenLoadoutMatchesAndLockInRange()
    {
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();
        await BindSlot0PulseAsync(client);
        await LockPrototypeTargetAlphaAsync(client);

        // Act
        var response = await client.PostAsJsonAsync(
            "/game/players/dev-local-1/ability-cast",
            new AbilityCastRequest
            {
                SchemaVersion = AbilityCastRequest.CurrentSchemaVersion,
                SlotIndex = 0,
                AbilityId = PrototypeAbilityRegistry.PrototypePulse,
                TargetId = PrototypeTargetRegistry.PrototypeTargetAlphaId,
            });

        // Assert
        var body = await response.Content.ReadFromJsonAsync<AbilityCastResponse>();
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        Assert.NotNull(body);
        Assert.True(body!.Accepted);
        Assert.Null(body.ReasonCode);
    }

    [Fact]
    public async Task PostAbilityCast_ShouldAccept_WithTargetId_WhenLoadoutMatches()
    {
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();
        await BindSlot0PulseAsync(client);
        await LockPrototypeTargetAlphaAsync(client);

        // Act
        var response = await client.PostAsJsonAsync(
            "/game/players/dev-local-1/ability-cast",
            new AbilityCastRequest
            {
                SchemaVersion = AbilityCastRequest.CurrentSchemaVersion,
                SlotIndex = 0,
                AbilityId = PrototypeAbilityRegistry.PrototypePulse,
                TargetId = "prototype_target_alpha",
            });

        // Assert
        var body = await response.Content.ReadFromJsonAsync<AbilityCastResponse>();
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        Assert.NotNull(body);
        Assert.True(body!.Accepted);
    }

    [Fact]
    public async Task PostAbilityCast_ShouldDenyInvalidTarget_WhenTargetIdMissing()
    {
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();
        await BindSlot0PulseAsync(client);
        await LockPrototypeTargetAlphaAsync(client);

        // Act
        var response = await client.PostAsJsonAsync(
            "/game/players/dev-local-1/ability-cast",
            new AbilityCastRequest
            {
                SchemaVersion = AbilityCastRequest.CurrentSchemaVersion,
                SlotIndex = 0,
                AbilityId = PrototypeAbilityRegistry.PrototypePulse,
                TargetId = null,
            });

        // Assert
        var body = await response.Content.ReadFromJsonAsync<AbilityCastResponse>();
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        Assert.NotNull(body);
        Assert.False(body!.Accepted);
        Assert.Equal(AbilityCastApi.ReasonInvalidTarget, body.ReasonCode);
    }

    [Fact]
    public async Task PostAbilityCast_ShouldDenyInvalidTarget_WhenUnknownTargetId()
    {
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();
        await BindSlot0PulseAsync(client);
        await LockPrototypeTargetAlphaAsync(client);

        // Act
        var response = await client.PostAsJsonAsync(
            "/game/players/dev-local-1/ability-cast",
            new AbilityCastRequest
            {
                SchemaVersion = AbilityCastRequest.CurrentSchemaVersion,
                SlotIndex = 0,
                AbilityId = PrototypeAbilityRegistry.PrototypePulse,
                TargetId = "not_a_registered_combat_target",
            });

        // Assert
        var body = await response.Content.ReadFromJsonAsync<AbilityCastResponse>();
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        Assert.NotNull(body);
        Assert.False(body!.Accepted);
        Assert.Equal(AbilityCastApi.ReasonInvalidTarget, body.ReasonCode);
    }

    [Fact]
    public async Task PostAbilityCast_ShouldDenyInvalidTarget_WhenTargetDoesNotMatchServerLock()
    {
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();
        await BindSlot0PulseAsync(client);
        await LockPrototypeTargetAlphaAsync(client);

        // Act
        var response = await client.PostAsJsonAsync(
            "/game/players/dev-local-1/ability-cast",
            new AbilityCastRequest
            {
                SchemaVersion = AbilityCastRequest.CurrentSchemaVersion,
                SlotIndex = 0,
                AbilityId = PrototypeAbilityRegistry.PrototypePulse,
                TargetId = PrototypeTargetRegistry.PrototypeTargetBetaId,
            });

        // Assert
        var body = await response.Content.ReadFromJsonAsync<AbilityCastResponse>();
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        Assert.NotNull(body);
        Assert.False(body!.Accepted);
        Assert.Equal(AbilityCastApi.ReasonInvalidTarget, body.ReasonCode);
    }

    [Fact]
    public async Task PostAbilityCast_ShouldDenyInvalidTarget_WhenNoServerLock()
    {
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();
        await BindSlot0PulseAsync(client);

        // Act
        var response = await client.PostAsJsonAsync(
            "/game/players/dev-local-1/ability-cast",
            new AbilityCastRequest
            {
                SchemaVersion = AbilityCastRequest.CurrentSchemaVersion,
                SlotIndex = 0,
                AbilityId = PrototypeAbilityRegistry.PrototypePulse,
                TargetId = PrototypeTargetRegistry.PrototypeTargetAlphaId,
            });

        // Assert
        var body = await response.Content.ReadFromJsonAsync<AbilityCastResponse>();
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        Assert.NotNull(body);
        Assert.False(body!.Accepted);
        Assert.Equal(AbilityCastApi.ReasonInvalidTarget, body.ReasonCode);
    }

    [Fact]
    public async Task PostAbilityCast_ShouldDenyOutOfRange_WhenAuthoritativePositionTooFarFromLock()
    {
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();
        await BindSlot0PulseAsync(client);
        await LockPrototypeTargetAlphaAsync(client);
        await TeleportDevPlayerAsync(client, 50.0, 0.9, 50.0);

        // Act
        var response = await client.PostAsJsonAsync(
            "/game/players/dev-local-1/ability-cast",
            new AbilityCastRequest
            {
                SchemaVersion = AbilityCastRequest.CurrentSchemaVersion,
                SlotIndex = 0,
                AbilityId = PrototypeAbilityRegistry.PrototypePulse,
                TargetId = PrototypeTargetRegistry.PrototypeTargetAlphaId,
            });

        // Assert
        var body = await response.Content.ReadFromJsonAsync<AbilityCastResponse>();
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        Assert.NotNull(body);
        Assert.False(body!.Accepted);
        Assert.Equal(AbilityCastApi.ReasonOutOfRange, body.ReasonCode);
    }

    [Fact]
    public async Task PostAbilityCast_ShouldDenySlotUnbound_WhenSlotEmpty()
    {
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();

        // Act
        var response = await client.PostAsJsonAsync(
            "/game/players/dev-local-1/ability-cast",
            new AbilityCastRequest
            {
                SchemaVersion = AbilityCastRequest.CurrentSchemaVersion,
                SlotIndex = 2,
                AbilityId = PrototypeAbilityRegistry.PrototypeGuard,
                TargetId = null,
            });

        // Assert
        var body = await response.Content.ReadFromJsonAsync<AbilityCastResponse>();
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        Assert.NotNull(body);
        Assert.False(body!.Accepted);
        Assert.Equal(AbilityCastApi.ReasonSlotUnbound, body.ReasonCode);
    }

    [Fact]
    public async Task PostAbilityCast_ShouldDenyLoadoutMismatch_WhenAbilityDoesNotMatchSlot()
    {
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();
        await BindSlot0PulseAsync(client);

        // Act
        var response = await client.PostAsJsonAsync(
            "/game/players/dev-local-1/ability-cast",
            new AbilityCastRequest
            {
                SchemaVersion = AbilityCastRequest.CurrentSchemaVersion,
                SlotIndex = 0,
                AbilityId = PrototypeAbilityRegistry.PrototypeBurst,
                TargetId = null,
            });

        // Assert
        var body = await response.Content.ReadFromJsonAsync<AbilityCastResponse>();
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        Assert.NotNull(body);
        Assert.False(body!.Accepted);
        Assert.Equal(AbilityCastApi.ReasonLoadoutMismatch, body.ReasonCode);
    }

    [Fact]
    public async Task PostAbilityCast_ShouldDenySlotOutOfBounds()
    {
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();
        await BindSlot0PulseAsync(client);

        // Act
        var response = await client.PostAsJsonAsync(
            "/game/players/dev-local-1/ability-cast",
            new AbilityCastRequest
            {
                SchemaVersion = AbilityCastRequest.CurrentSchemaVersion,
                SlotIndex = 8,
                AbilityId = PrototypeAbilityRegistry.PrototypePulse,
                TargetId = null,
            });

        // Assert
        var body = await response.Content.ReadFromJsonAsync<AbilityCastResponse>();
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        Assert.NotNull(body);
        Assert.False(body!.Accepted);
        Assert.Equal(AbilityCastApi.ReasonSlotOutOfBounds, body.ReasonCode);
    }

    [Fact]
    public async Task PostAbilityCast_ShouldDenyUnknownAbility_WhenAbilityIdNotInRegistry()
    {
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();
        await BindSlot0PulseAsync(client);

        // Act
        var response = await client.PostAsJsonAsync(
            "/game/players/dev-local-1/ability-cast",
            new AbilityCastRequest
            {
                SchemaVersion = AbilityCastRequest.CurrentSchemaVersion,
                SlotIndex = 0,
                AbilityId = "not_a_real_ability",
                TargetId = null,
            });

        // Assert
        var body = await response.Content.ReadFromJsonAsync<AbilityCastResponse>();
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        Assert.NotNull(body);
        Assert.False(body!.Accepted);
        Assert.Equal(AbilityCastApi.ReasonUnknownAbility, body.ReasonCode);
    }

    [Fact]
    public async Task PostAbilityCast_ShouldReturnBadRequest_WhenBodyMalformed()
    {
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();
        var malformed = new StringContent(
            "{\"schemaVersion\":1,\"slotIndex\":",
            Encoding.UTF8,
            "application/json");

        // Act
        var response = await client.PostAsync("/game/players/dev-local-1/ability-cast", malformed);

        // Assert
        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
    }

    [Fact]
    public async Task PostAbilityCast_ShouldReturnBadRequest_WhenSchemaVersionWrong()
    {
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();
        var request = new AbilityCastRequest
        {
            SchemaVersion = 999,
            SlotIndex = 0,
            AbilityId = PrototypeAbilityRegistry.PrototypePulse,
        };

        // Act
        var response = await client.PostAsJsonAsync("/game/players/dev-local-1/ability-cast", request);

        // Assert
        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
    }

    [Fact]
    public async Task PostAbilityCast_ShouldReturnNotFound_WhenPlayerUnknown()
    {
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();
        var request = new AbilityCastRequest
        {
            SchemaVersion = AbilityCastRequest.CurrentSchemaVersion,
            SlotIndex = 0,
            AbilityId = PrototypeAbilityRegistry.PrototypePulse,
        };

        // Act
        var response = await client.PostAsJsonAsync("/game/players/missing-player/ability-cast", request);

        // Assert
        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
    }

    [Fact]
    public async Task PostAbilityCast_ShouldDenyOnCooldown_WhenRepeatedWhileCooling()
    {
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();
        await BindSlot0PulseAsync(client);
        await LockPrototypeTargetAlphaAsync(client);
        var cast = new AbilityCastRequest
        {
            SchemaVersion = AbilityCastRequest.CurrentSchemaVersion,
            SlotIndex = 0,
            AbilityId = PrototypeAbilityRegistry.PrototypePulse,
            TargetId = PrototypeTargetRegistry.PrototypeTargetAlphaId,
        };

        // Act
        var first = await client.PostAsJsonAsync("/game/players/dev-local-1/ability-cast", cast);
        var second = await client.PostAsJsonAsync("/game/players/dev-local-1/ability-cast", cast);

        // Assert
        var body1 = await first.Content.ReadFromJsonAsync<AbilityCastResponse>();
        var body2 = await second.Content.ReadFromJsonAsync<AbilityCastResponse>();
        Assert.Equal(HttpStatusCode.OK, first.StatusCode);
        Assert.Equal(HttpStatusCode.OK, second.StatusCode);
        Assert.NotNull(body1);
        Assert.NotNull(body2);
        Assert.True(body1!.Accepted);
        Assert.False(body2!.Accepted);
        Assert.Equal(AbilityCastApi.ReasonOnCooldown, body2.ReasonCode);
    }

    [Fact]
    public async Task PostAbilityCast_ShouldAcceptAgain_WhenCooldownElapsed()
    {
        // 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 first = await client.PostAsJsonAsync("/game/players/dev-local-1/ability-cast", cast);
        factory.FakeClock!.Advance(AbilityPrototypeCooldown.GlobalDuration + TimeSpan.FromMilliseconds(50));
        var second = await client.PostAsJsonAsync("/game/players/dev-local-1/ability-cast", cast);

        // Assert
        var body1 = await first.Content.ReadFromJsonAsync<AbilityCastResponse>();
        var body2 = await second.Content.ReadFromJsonAsync<AbilityCastResponse>();
        Assert.Equal(HttpStatusCode.OK, first.StatusCode);
        Assert.Equal(HttpStatusCode.OK, second.StatusCode);
        Assert.NotNull(body1);
        Assert.NotNull(body2);
        Assert.True(body1!.Accepted);
        Assert.True(body2!.Accepted);
    }
}
