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

namespace NeonSprawl.Server.Tests.Game.Targeting;

/// <summary>Integration tests for <see cref="TargetingApi"/> (NEO-23).</summary>
public class TargetingApiTests
{
    [Fact]
    public async Task GetTarget_ShouldReturnNone_WhenNoLock()
    {
        // Arrange

        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();
        // Act

        var response = await client.GetAsync("/game/players/dev-local-1/target");
        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        var body = await response.Content.ReadFromJsonAsync<PlayerTargetStateResponse>();
        Assert.NotNull(body);
        Assert.Equal(PlayerTargetStateResponse.CurrentSchemaVersion, body!.SchemaVersion);
        Assert.Equal("dev-local-1", body.PlayerId);
        Assert.Null(body.LockedTargetId);
        Assert.Equal(TargetValidity.None, body.Validity);
        Assert.Equal(0, body.Sequence);
    }

    [Fact]
    public async Task PostSelect_ShouldApplyLock_WhenInRange()
    {
        // Arrange

        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();
        // Act

        var response = await client.PostAsJsonAsync(
            "/game/players/dev-local-1/target/select",
            new TargetSelectRequest
            {
                SchemaVersion = TargetSelectRequest.CurrentSchemaVersion,
                TargetId = PrototypeTargetRegistry.PrototypeTargetAlphaId,
            });
        // Assert

        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        var body = await response.Content.ReadFromJsonAsync<TargetSelectResponse>();
        Assert.NotNull(body);
        Assert.True(body!.SelectionApplied);
        Assert.Null(body.ReasonCode);
        Assert.Equal(PrototypeTargetRegistry.PrototypeTargetAlphaId, body.TargetState.LockedTargetId);
        Assert.Equal(TargetValidity.Ok, body.TargetState.Validity);
        Assert.Equal(1, body.TargetState.Sequence);
    }

    [Fact]
    public async Task PostSelect_ShouldDenyUnknownTarget_WithReasonCode()
    {
        // Arrange

        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();
        // Act

        var response = await client.PostAsJsonAsync(
            "/game/players/dev-local-1/target/select",
            new TargetSelectRequest
            {
                SchemaVersion = TargetSelectRequest.CurrentSchemaVersion,
                TargetId = "not_a_real_target",
            });
        // Assert

        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        var body = await response.Content.ReadFromJsonAsync<TargetSelectResponse>();
        Assert.NotNull(body);
        Assert.False(body!.SelectionApplied);
        Assert.Equal(TargetingApi.ReasonUnknownTarget, body.ReasonCode);
        Assert.Null(body.TargetState.LockedTargetId);
        Assert.Equal(TargetValidity.None, body.TargetState.Validity);
    }

    [Fact]
    public async Task PostSelect_ShouldDenyOutOfRange_WhenBetaFarFromSpawn()
    {
        // Arrange

        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();
        // Act

        var response = await client.PostAsJsonAsync(
            "/game/players/dev-local-1/target/select",
            new TargetSelectRequest
            {
                SchemaVersion = TargetSelectRequest.CurrentSchemaVersion,
                TargetId = PrototypeTargetRegistry.PrototypeTargetBetaId,
            });
        // Assert

        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        var body = await response.Content.ReadFromJsonAsync<TargetSelectResponse>();
        Assert.NotNull(body);
        Assert.False(body!.SelectionApplied);
        Assert.Equal(TargetingApi.ReasonOutOfRange, body.ReasonCode);
        Assert.Null(body.TargetState.LockedTargetId);
    }

    [Fact]
    public async Task PostSelect_ShouldUsePositionHint_WhenStoredSnapIsStale()
    {
        // NEO-24 follow-up #5: without the hint, spawn's stored position (-5, -5) denies beta (r=6,
        // anchor (3, 3)) — ~11.3 m out. With an advisory `positionHint` pinning the capsule inside
        // beta's ring, the server must accept. This is exactly the race-free path the client uses.
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();
        // Act

        var response = await client.PostAsJsonAsync(
            "/game/players/dev-local-1/target/select",
            new TargetSelectRequest
            {
                SchemaVersion = TargetSelectRequest.CurrentSchemaVersion,
                TargetId = PrototypeTargetRegistry.PrototypeTargetBetaId,
                PositionHint = new PositionVector { X = 3.0, Y = 0.5, Z = 5.0 },
            });
        // Assert

        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        var body = await response.Content.ReadFromJsonAsync<TargetSelectResponse>();
        Assert.NotNull(body);
        Assert.True(body!.SelectionApplied);
        Assert.Null(body.ReasonCode);
        Assert.Equal(PrototypeTargetRegistry.PrototypeTargetBetaId, body.TargetState.LockedTargetId);
    }

    [Fact]
    public async Task PostSelect_ShouldDenyOutOfRange_WhenHintIsFar()
    {
        // Hint that places the capsule outside beta's ring must deny — hint is advisory only;
        // it does not bypass the radius check.
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();
        // Act

        var response = await client.PostAsJsonAsync(
            "/game/players/dev-local-1/target/select",
            new TargetSelectRequest
            {
                SchemaVersion = TargetSelectRequest.CurrentSchemaVersion,
                TargetId = PrototypeTargetRegistry.PrototypeTargetBetaId,
                PositionHint = new PositionVector { X = 50.0, Y = 0.0, Z = 50.0 },
            });
        // Assert

        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        var body = await response.Content.ReadFromJsonAsync<TargetSelectResponse>();
        Assert.NotNull(body);
        Assert.False(body!.SelectionApplied);
        Assert.Equal(TargetingApi.ReasonOutOfRange, body.ReasonCode);
    }

    [Fact]
    public async Task PostSelect_PositionHint_ShouldNotWriteToPositionStore()
    {
        // Hint must be purely advisory for the range check. Confirm the stored `PositionState`
        // is untouched by a select-with-hint by reading it back through `GET /position`.
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();
        var before = await client.GetFromJsonAsync<PositionStateResponse>("/game/players/dev-local-1/position");

        // Act
        var selectResponse = await client.PostAsJsonAsync(
            "/game/players/dev-local-1/target/select",
            new TargetSelectRequest
            {
                SchemaVersion = TargetSelectRequest.CurrentSchemaVersion,
                TargetId = PrototypeTargetRegistry.PrototypeTargetBetaId,
                PositionHint = new PositionVector { X = 3.0, Y = 0.5, Z = 5.0 },
            });

        // Assert
        Assert.NotNull(before);
        Assert.Equal(HttpStatusCode.OK, selectResponse.StatusCode);
        var after = await client.GetFromJsonAsync<PositionStateResponse>("/game/players/dev-local-1/position");
        Assert.NotNull(after);
        Assert.Equal(before!.Position.X, after!.Position.X);
        Assert.Equal(before.Position.Y, after.Position.Y);
        Assert.Equal(before.Position.Z, after.Position.Z);
        Assert.Equal(before.Sequence, after.Sequence);
    }

    [Fact]
    public async Task PostSelect_ShouldClearLock_WhenTargetIdOmitted()
    {
        // Arrange

        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();

        // Act
        var setResponse = await client.PostAsJsonAsync(
            "/game/players/dev-local-1/target/select",
            new TargetSelectRequest
            {
                SchemaVersion = TargetSelectRequest.CurrentSchemaVersion,
                TargetId = PrototypeTargetRegistry.PrototypeTargetAlphaId,
            });

        var clear = await client.PostAsync(
            "/game/players/dev-local-1/target/select",
            new StringContent(
                $"{{\"schemaVersion\":{TargetSelectRequest.CurrentSchemaVersion}}}",
                Encoding.UTF8,
                "application/json"));

        // Assert
        var body = await clear.Content.ReadFromJsonAsync<TargetSelectResponse>();
        Assert.Equal(HttpStatusCode.OK, setResponse.StatusCode);
        Assert.Equal(HttpStatusCode.OK, clear.StatusCode);
        Assert.NotNull(body);
        Assert.True(body!.SelectionApplied);
        Assert.Null(body.TargetState.LockedTargetId);
        Assert.Equal(TargetValidity.None, body.TargetState.Validity);
    }

    [Fact]
    public async Task GetTarget_ShouldShowOutOfRange_AfterMoveWithoutSelect()
    {
        // Arrange

        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();

        // Act
        var selectResponse = await client.PostAsJsonAsync(
            "/game/players/dev-local-1/target/select",
            new TargetSelectRequest
            {
                SchemaVersion = TargetSelectRequest.CurrentSchemaVersion,
                TargetId = PrototypeTargetRegistry.PrototypeTargetAlphaId,
            });

        var move = new MoveCommandRequest
        {
            SchemaVersion = MoveCommandRequest.CurrentSchemaVersion,
            Target = new PositionVector { X = 50, Y = 0, Z = 50 },
        };
        var moveResponse = await client.PostAsJsonAsync("/game/players/dev-local-1/move", move);

        var get = await client.GetAsync("/game/players/dev-local-1/target");

        // Assert
        var body = await get.Content.ReadFromJsonAsync<PlayerTargetStateResponse>();
        Assert.Equal(HttpStatusCode.OK, selectResponse.StatusCode);
        Assert.Equal(HttpStatusCode.OK, moveResponse.StatusCode);
        Assert.Equal(HttpStatusCode.OK, get.StatusCode);
        Assert.NotNull(body);
        Assert.Equal(PrototypeTargetRegistry.PrototypeTargetAlphaId, body!.LockedTargetId);
        Assert.Equal(TargetValidity.OutOfRange, body.Validity);
    }

    [Fact]
    public async Task GetTarget_ShouldReturnNotFound_WhenPlayerUnknown()
    {
        // Arrange

        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();
        // Act

        var response = await client.GetAsync("/game/players/missing-player/target");
        // Assert
        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
    }

    [Fact]
    public async Task PostSelect_ShouldReturnNotFound_WhenPlayerUnknown()
    {
        // Arrange

        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();
        // Act

        var response = await client.PostAsJsonAsync(
            "/game/players/missing-player/target/select",
            new TargetSelectRequest
            {
                SchemaVersion = TargetSelectRequest.CurrentSchemaVersion,
                TargetId = PrototypeTargetRegistry.PrototypeTargetAlphaId,
            });
        // Assert

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

    [Fact]
    public async Task PostSelect_ShouldReturnBadRequest_WhenSchemaVersionWrong()
    {
        // Arrange

        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();
        // Act

        var response = await client.PostAsJsonAsync(
            "/game/players/dev-local-1/target/select",
            new TargetSelectRequest
            {
                SchemaVersion = 999,
                TargetId = PrototypeTargetRegistry.PrototypeTargetAlphaId,
            });
        // Assert

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

    [Fact]
    public async Task PostSelect_ShouldReturnBadRequest_WhenTargetIdWhitespaceOnly()
    {
        // Arrange

        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();
        // Act

        var response = await client.PostAsJsonAsync(
            "/game/players/dev-local-1/target/select",
            new TargetSelectRequest
            {
                SchemaVersion = TargetSelectRequest.CurrentSchemaVersion,
                TargetId = "  \t ",
            });
        // Assert

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

    [Fact]
    public async Task PostSelect_ShouldBeCaseInsensitive_ForTargetId()
    {
        // Arrange

        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();
        // Act

        var response = await client.PostAsJsonAsync(
            "/game/players/dev-local-1/target/select",
            new TargetSelectRequest
            {
                SchemaVersion = TargetSelectRequest.CurrentSchemaVersion,
                TargetId = "  PROTOTYPE_TARGET_ALPHA  ",
            });
        // Assert

        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        var body = await response.Content.ReadFromJsonAsync<TargetSelectResponse>();
        Assert.NotNull(body);
        Assert.True(body!.SelectionApplied);
        Assert.Equal(PrototypeTargetRegistry.PrototypeTargetAlphaId, body.TargetState.LockedTargetId);
    }

    [Fact]
    public async Task PostSelect_ShouldStayIdempotent_WhenSameTargetReselect()
    {
        // Arrange

        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();

        var req = new TargetSelectRequest
        {
            SchemaVersion = TargetSelectRequest.CurrentSchemaVersion,
            TargetId = PrototypeTargetRegistry.PrototypeTargetAlphaId,
        };
        // Act
        var first = await client.PostAsJsonAsync("/game/players/dev-local-1/target/select", req);
        var second = await client.PostAsJsonAsync("/game/players/dev-local-1/target/select", req);
        // Assert
        Assert.Equal(HttpStatusCode.OK, first.StatusCode);
        Assert.Equal(HttpStatusCode.OK, second.StatusCode);
        var b1 = await first.Content.ReadFromJsonAsync<TargetSelectResponse>();
        var b2 = await second.Content.ReadFromJsonAsync<TargetSelectResponse>();
        Assert.Equal(b1!.TargetState.Sequence, b2!.TargetState.Sequence);
    }

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

        await client.PostAsJsonAsync(
            "/game/players/dev-local-1/target/select",
            new TargetSelectRequest
            {
                SchemaVersion = TargetSelectRequest.CurrentSchemaVersion,
                TargetId = PrototypeTargetRegistry.PrototypeTargetAlphaId,
            });

        var move = new MoveCommandRequest
        {
            SchemaVersion = MoveCommandRequest.CurrentSchemaVersion,
            Target = new PositionVector { X = 50, Y = 0, Z = 50 },
        };
        await client.PostAsJsonAsync("/game/players/dev-local-1/move", move);

        // Act
        var response = await client.PostAsJsonAsync(
            "/game/players/dev-local-1/target/select",
            new TargetSelectRequest
            {
                SchemaVersion = TargetSelectRequest.CurrentSchemaVersion,
                TargetId = PrototypeTargetRegistry.PrototypeTargetAlphaId,
            });

        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        var body = await response.Content.ReadFromJsonAsync<TargetSelectResponse>();
        Assert.NotNull(body);
        Assert.False(body!.SelectionApplied);
        Assert.Equal(TargetingApi.ReasonOutOfRange, body.ReasonCode);
        Assert.Equal(PrototypeTargetRegistry.PrototypeTargetAlphaId, body.TargetState.LockedTargetId);
        Assert.Equal(TargetValidity.OutOfRange, body.TargetState.Validity);
    }
}
