using System.Net;
using System.Net.Http.Json;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using NeonSprawl.Server.Game.PositionState;
using Npgsql;
using Xunit;

namespace NeonSprawl.Server.Tests.Game.PositionState;

/// <summary>NS-17: real PostgreSQL + HTTP; collection serializes tests that share the database.</summary>
[Collection("Postgres integration")]
public sealed class PostgresPositionStateIntegrationTests(PostgresIntegrationHarness harness)
{
    private PostgresWebApplicationFactory Factory => harness.Factory;
    [RequirePostgresFact]
    public async Task PostMove_ThenGet_ShouldReflectPersistedPositionAndSequence()
    {
        // Arrange
        await ResetPlayerPositionTableAsync();
        using var client = Factory.CreateClient();
        var cmd = new MoveCommandRequest
        {
            SchemaVersion = MoveCommandRequest.CurrentSchemaVersion,
            Target = new PositionVector { X = 2, Y = 0.5, Z = 3.5 },
        };
        var before = await client.GetFromJsonAsync<PositionStateResponse>("/game/players/dev-local-1/position");

        // Act
        var post = await client.PostAsJsonAsync("/game/players/dev-local-1/move", cmd);

        // Assert
        Assert.NotNull(before);
        Assert.Equal(HttpStatusCode.OK, post.StatusCode);
        var postBody = await post.Content.ReadFromJsonAsync<PositionStateResponse>();
        var after = await client.GetFromJsonAsync<PositionStateResponse>("/game/players/dev-local-1/position");
        Assert.NotNull(postBody);
        Assert.NotNull(after);
        Assert.Equal(PositionStateResponse.CurrentSchemaVersion, postBody!.SchemaVersion);
        Assert.Equal(before!.Sequence + 1, postBody.Sequence);
        Assert.Equal(2, postBody.Position.X);
        Assert.Equal(0.5, postBody.Position.Y);
        Assert.Equal(3.5, postBody.Position.Z);
        Assert.Equal(postBody.Sequence, after!.Sequence);
        Assert.Equal(postBody.Position.X, after.Position.X);
        Assert.Equal(postBody.Position.Y, after.Position.Y);
        Assert.Equal(postBody.Position.Z, after.Position.Z);
    }

    [RequirePostgresFact]
    public async Task SecondProcess_ShouldReadLastPosition_AfterFirstFactoryDisposed()
    {
        // Arrange
        await ResetPlayerPositionTableAsync();
        var cmd = new MoveCommandRequest
        {
            SchemaVersion = MoveCommandRequest.CurrentSchemaVersion,
            Target = new PositionVector { X = 7, Y = 0.8, Z = 7 },
        };

        HttpStatusCode postStatus = default;
        PositionStateResponse? persisted = null;

        // Act
        using (var first = Factory.CreateClient())
        {
            var post = await first.PostAsJsonAsync("/game/players/dev-local-1/move", cmd);
            postStatus = post.StatusCode;
            persisted = await post.Content.ReadFromJsonAsync<PositionStateResponse>();
        }

        // Assert
        await using var secondFactory = new PostgresWebApplicationFactory();
        using var secondClient = secondFactory.CreateClient();
        var after = await secondClient.GetFromJsonAsync<PositionStateResponse>("/game/players/dev-local-1/position");
        Assert.Equal(HttpStatusCode.OK, postStatus);
        Assert.NotNull(persisted);
        Assert.NotNull(after);
        Assert.Equal(7, after!.Position.X);
        Assert.Equal(0.8, after.Position.Y);
        Assert.Equal(7, after.Position.Z);
        Assert.Equal(1ul, after.Sequence);
    }

    [RequirePostgresFact]
    public async Task PostMove_ShouldReturnNotFound_WhenPlayerUnknown()
    {
        // Arrange
        await ResetPlayerPositionTableAsync();
        using var client = Factory.CreateClient();
        var cmd = new MoveCommandRequest
        {
            SchemaVersion = MoveCommandRequest.CurrentSchemaVersion,
            Target = new PositionVector { X = 0, Y = 0, Z = 0 },
        };

        // Act
        var response = await client.PostAsJsonAsync("/game/players/not-a-seeded-player/move", cmd);

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

    /// <summary>DDL + empty table + dev row matching the test host's <see cref="GamePositionOptions"/> (parity with startup seed).</summary>
    private async Task ResetPlayerPositionTableAsync()
    {
        var cs = Environment.GetEnvironmentVariable("ConnectionStrings__NeonSprawl");
        if (string.IsNullOrWhiteSpace(cs))
        {
            throw new InvalidOperationException("ConnectionStrings__NeonSprawl is not set.");
        }

        _ = Factory.Services;
        var options = Factory.Services.GetRequiredService<IOptions<GamePositionOptions>>().Value;

        var ddlPath = Path.Combine(AppContext.BaseDirectory, "db", "migrations", "V001__player_position.sql");
        if (!File.Exists(ddlPath))
        {
            throw new FileNotFoundException($"Test DDL not found at '{ddlPath}'.", ddlPath);
        }

        var ddl = await File.ReadAllTextAsync(ddlPath);
        await using var conn = new NpgsqlConnection(cs);
        await conn.OpenAsync();
        await using (var apply = new NpgsqlCommand(ddl, conn))
        {
            await apply.ExecuteNonQueryAsync();
        }

        await using (var truncate = new NpgsqlCommand("TRUNCATE player_position CASCADE;", conn))
        {
            await truncate.ExecuteNonQueryAsync();
        }

        PostgresPositionBootstrap.SeedDevPlayer(conn, options);
    }
}
