using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using GameList.Domain;
using GameList.Tests.Support;
using Microsoft.EntityFrameworkCore;
using GameList.Data;
using Microsoft.Extensions.DependencyInjection;

namespace GameList.Tests;

public class StateTests
{
    [Fact]
    public async Task State_endpoint_returns_expected_payload_for_authenticated_user()
    {
        await using var factory = new TestWebApplicationFactory();
        var client = factory.CreateClientWithCookies();
        await client.RegisterAsync("payload");
        await factory.WithDbContextAsync(async db =>
        {
            var player = await db.Players.FirstAsync();
            player.HasJoker = true;
            await db.SaveChangesAsync();
        });
        await client.CreateSuggestionAsync("One");

        var state = await client.GetFromJsonAsync<JsonElement>("/api/state");

        Assert.Equal(nameof(Phase.Suggest), state.GetProperty("currentPhase").GetString());
        Assert.False(state.GetProperty("votesFinal").GetBoolean());
        Assert.True(state.GetProperty("hasJoker").GetBoolean());
        Assert.True(state.GetProperty("players").GetInt32() >= 1);
        Assert.True(state.GetProperty("suggestions").GetInt32() >= 1);
        Assert.True(state.GetProperty("votes").GetInt32() >= 0);
    }

    [Fact]
    public async Task GetCurrentPhase_reads_effective_phase_without_mutating_player()
    {
        await using var factory = new TestWebApplicationFactory();
        Guid playerId = Guid.Empty;
        await factory.WithDbContextAsync(async db =>
        {
            var player = new Player
            {
                Id = Guid.NewGuid(),
                Username = "legacy",
                NormalizedUsername = "legacy",
                PasswordHash = [1],
                PasswordSalt = [1],
                DisplayName = "Legacy",
                CurrentPhase = Phase.Reveal,
                VotesFinal = true
            };
            playerId = player.Id;
            db.Players.Add(player);
            var state = await db.AppState.FirstAsync();
            state.ResultsOpen = true;
            await db.SaveChangesAsync();
        });

        using (var scope = factory.Services.CreateScope())
        {
            var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
            var phase = await Endpoints.EndpointHelpers.GetCurrentPhaseAsync(db, playerId);
            Assert.Equal(Phase.Results, phase);
        }

        await factory.WithDbContextAsync(async db =>
        {
            var state = await db.AppState.FirstAsync();
            state.ResultsOpen = false;
            await db.SaveChangesAsync();
        });

        using (var scope = factory.Services.CreateScope())
        {
            var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
            var phase = await Endpoints.EndpointHelpers.GetCurrentPhaseAsync(db, playerId);
            var player = await db.Players.FindAsync(playerId);
            Assert.Equal(Phase.Vote, phase);
            Assert.Equal(Phase.Reveal, player!.CurrentPhase);
            Assert.True(player.VotesFinal);
        }
    }

    [Fact]
    public async Task Phase_next_advances_and_clears_votesfinal()
    {
        await using var factory = new TestWebApplicationFactory();
        var client = factory.CreateClientWithCookies();
        await client.RegisterAsync("advance");

        await factory.WithDbContextAsync(async db =>
        {
            var player = await db.Players.FirstAsync();
            player.VotesFinal = true;
            await db.SaveChangesAsync();
        });

        var toVote = await client.PostAsJsonAsync("/api/me/phase/next", new { });
        toVote.EnsureSuccessStatusCode();
        var toResultsLocked = await client.PostAsJsonAsync("/api/me/phase/next", new { });
        Assert.Equal(HttpStatusCode.BadRequest, toResultsLocked.StatusCode);

        // unlock results and advance
        var admin = factory.CreateClientWithCookies();
        await admin.RegisterAsync("admin", admin: true);
        await admin.PostAsJsonAsync("/api/admin/results", new { resultsOpen = true });
        var toResults = await client.PostAsJsonAsync("/api/me/phase/next", new { });
        toResults.EnsureSuccessStatusCode();
        var me = await client.GetFromJsonAsync<JsonElement>("/api/me");
        Assert.False(me.GetProperty("votesFinal").GetBoolean());
        Assert.Equal(nameof(Phase.Results), me.GetProperty("currentPhase").GetString());
    }

    [Fact]
    public async Task Phase_prev_moves_back_and_clears_votesfinal()
    {
        await using var factory = new TestWebApplicationFactory();
        var admin = factory.CreateClientWithCookies();
        await admin.RegisterAsync("admin", admin: true);

        await admin.PostAsJsonAsync("/api/me/phase/next", new { }); // Vote
        await factory.WithDbContextAsync(async db =>
        {
            var player = await db.Players.FirstAsync();
            player.VotesFinal = true;
            await db.SaveChangesAsync();
        });
        var backToSuggest = await admin.PostAsJsonAsync("/api/me/phase/prev", new { });
        backToSuggest.EnsureSuccessStatusCode();

        var me = await admin.GetFromJsonAsync<JsonElement>("/api/me");
        Assert.Equal(nameof(Phase.Suggest), me.GetProperty("currentPhase").GetString());
        Assert.False(me.GetProperty("votesFinal").GetBoolean());
    }

    [Fact]
    public async Task Cannot_advance_to_results_when_locked()
    {
        await using var factory = new TestWebApplicationFactory();
        var client = factory.CreateClientWithCookies();
        await client.RegisterAsync("player");

        var toVote = await client.PostAsync("/api/me/phase/next", JsonContent.Create(new { }));
        toVote.EnsureSuccessStatusCode();

        var toResults = await client.PostAsync("/api/me/phase/next", JsonContent.Create(new { }));

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

    [Fact]
    public async Task Admin_opening_results_moves_players_to_results_phase()
    {
        await using var factory = new TestWebApplicationFactory();
        var admin = factory.CreateClientWithCookies();
        await admin.RegisterAsync("admin", admin: true);

        var player = factory.CreateClientWithCookies();
        await player.RegisterAsync("user");

        var toggle = await admin.PostAsJsonAsync("/api/admin/results", new { resultsOpen = true });
        toggle.EnsureSuccessStatusCode();

        var state = await player.GetFromJsonAsync<JsonElement>("/api/state");

        Assert.Equal(nameof(Phase.Results), state.GetProperty("currentPhase").GetString());
        Assert.True(state.GetProperty("resultsOpen").GetBoolean());
    }

    [Fact]
    public async Task Display_name_cannot_be_changed_after_registration()
    {
        await using var factory = new TestWebApplicationFactory();
        var client = factory.CreateClientWithCookies();
        var username = "fixedname";
        await client.RegisterAsync(username);
        var originalDisplay = $"{username}-name";

        var attempt = await client.PostAsJsonAsync("/api/me/name", new { name = "New Name" });
        Assert.Equal(HttpStatusCode.NotFound, attempt.StatusCode);

        var me = await client.GetFromJsonAsync<JsonElement>("/api/me");
        Assert.Equal(originalDisplay, me.GetProperty("displayName").GetString());
    }

    [Fact]
    public async Task Phase_prev_admin_only()
    {
        await using var factory = new TestWebApplicationFactory();
        var player = factory.CreateClientWithCookies();
        await player.RegisterAsync("phase");

        var notAdmin = await player.PostAsJsonAsync("/api/me/phase/prev", new { });
        Assert.Equal(HttpStatusCode.BadRequest, notAdmin.StatusCode);

        var admin = factory.CreateClientWithCookies();
        await admin.RegisterAsync("admin", admin: true);
        await admin.PostAsJsonAsync("/api/me/phase/next", new { }); // to Vote
        var back = await admin.PostAsJsonAsync("/api/me/phase/prev", new { });
        back.EnsureSuccessStatusCode();
        var me = await admin.GetFromJsonAsync<JsonElement>("/api/me");
        Assert.Equal(nameof(Phase.Suggest), me.GetProperty("currentPhase").GetString());
    }

    [Fact]
    public async Task State_endpoint_requires_auth_and_counts()
    {
        await using var factory = new TestWebApplicationFactory();
        var anon = factory.CreateClient();
        var unauthorized = await anon.GetAsync("/api/state");
        Assert.Equal(HttpStatusCode.Unauthorized, unauthorized.StatusCode);

        var client = factory.CreateClientWithCookies();
        await client.RegisterAsync("counting");
        await client.CreateSuggestionAsync("One");
        var state = await client.GetFromJsonAsync<JsonElement>("/api/state");
        Assert.True(state.TryGetProperty("Players", out var players) || state.TryGetProperty("players", out players));
        Assert.True(players.GetInt32() >= 1);
        Assert.True(state.TryGetProperty("Suggestions", out var suggestions) || state.TryGetProperty("suggestions", out suggestions));
        Assert.True(suggestions.GetInt32() >= 1);
    }

    [Fact]
    public async Task State_endpoint_with_stale_cookie_returns_unauthorized_and_clears_cookie()
    {
        await using var factory = new TestWebApplicationFactory();
        var client = factory.CreateClientWithCookies();
        await client.RegisterAsync("stale");

        await factory.WithDbContextAsync(async db =>
        {
            var player = await db.Players.FirstAsync();
            db.Players.Remove(player);
            await db.SaveChangesAsync();
        });

        var resp = await client.GetAsync("/api/state");

        Assert.Equal(HttpStatusCode.Unauthorized, resp.StatusCode);
        Assert.True(resp.Headers.TryGetValues("Set-Cookie", out var cookies));
        Assert.Contains(cookies, c => c.Contains("player=", StringComparison.OrdinalIgnoreCase));
    }

    [Fact]
    public async Task Health_endpoint_ok()
    {
        await using var factory = new TestWebApplicationFactory();
        var resp = await factory.CreateClient().GetFromJsonAsync<JsonElement>("/health");
        Assert.Equal("ok", resp.GetProperty("status").GetString());
    }

    [Fact]
    public async Task State_and_me_reads_do_not_persist_phase_reconciliation()
    {
        await using var factory = new TestWebApplicationFactory();
        var client = factory.CreateClientWithCookies();
        await client.RegisterAsync("reader");

        await factory.WithDbContextAsync(async db =>
        {
            var player = await db.Players.FirstAsync();
            player.CurrentPhase = Phase.Results;
            player.VotesFinal = true;
            var state = await db.AppState.FirstAsync();
            state.ResultsOpen = false;
            await db.SaveChangesAsync();
        });

        var stateResponse = await client.GetFromJsonAsync<JsonElement>("/api/state");
        var meResponse = await client.GetFromJsonAsync<JsonElement>("/api/me");

        Assert.Equal(nameof(Phase.Vote), stateResponse.GetProperty("currentPhase").GetString());
        Assert.Equal(nameof(Phase.Vote), meResponse.GetProperty("currentPhase").GetString());

        await factory.WithDbContextAsync(async db =>
        {
            var player = await db.Players.AsNoTracking().FirstAsync();
            Assert.Equal(Phase.Results, player.CurrentPhase);
            Assert.True(player.VotesFinal);
        });
    }

    [Fact]
    public async Task GetPhase_aligns_to_results_when_open()
    {
        await using var factory = new TestWebApplicationFactory();
        await factory.WithDbContextAsync(async db =>
        {
            var player = new Player
            {
                Id = Guid.NewGuid(),
                Username = "phase",
                NormalizedUsername = "phase",
                PasswordHash = [1],
                PasswordSalt = [1],
                DisplayName = "phase",
                CurrentPhase = Phase.Vote
            };
            db.Players.Add(player);
            var state = await db.AppState.FirstAsync();
            state.ResultsOpen = true;
            await db.SaveChangesAsync();
        });

        using var scope = factory.Services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        var playerId = await db.Players.Select(p => p.Id).FirstAsync();
        var phase = await Endpoints.EndpointHelpers.GetCurrentPhaseAsync(db, playerId);

        Assert.Equal(Phase.Results, phase);
    }
}

