using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using GameList.Tests.Support;
using Microsoft.EntityFrameworkCore;

namespace GameList.Tests;

public class SuggestionTests
{
    [Fact]
    public async Task Player_cannot_exceed_five_suggestions()
    {
        await using var factory = new TestWebApplicationFactory();
        var client = factory.CreateClientWithCookies();
        await client.RegisterAsync("suggestor");

        for (var i = 0; i < 5; i++)
        {
            var resp = await client.PostAsJsonAsync("/api/suggestions", new
            {
                Name = $"Game {i}",
                Genre = (string?)null,
                Description = (string?)null,
                ScreenshotUrl = (string?)null,
                YoutubeUrl = (string?)null,
                GameUrl = (string?)null,
                MinPlayers = (int?)null,
                MaxPlayers = (int?)null
            });
            resp.EnsureSuccessStatusCode();
        }

        var sixth = await client.PostAsJsonAsync("/api/suggestions", new
        {
            Name = "Overflow",
            Genre = (string?)null,
            Description = (string?)null,
            ScreenshotUrl = (string?)null,
            YoutubeUrl = (string?)null,
            GameUrl = (string?)null,
            MinPlayers = (int?)null,
            MaxPlayers = (int?)null
        });

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

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

        var badExt = await client.PostAsJsonAsync("/api/suggestions", new
        {
            Name = "BadImg",
            Genre = (string?)null,
            Description = (string?)null,
            ScreenshotUrl = "http://example.com/file.txt",
            YoutubeUrl = (string?)null,
            GameUrl = (string?)null,
            MinPlayers = (int?)null,
            MaxPlayers = (int?)null
        });
        Assert.Equal(HttpStatusCode.BadRequest, badExt.StatusCode);

        var badPlayers = await client.PostAsJsonAsync("/api/suggestions", new
        {
            Name = "BadPlayers",
            Genre = (string?)null,
            Description = (string?)null,
            ScreenshotUrl = (string?)null,
            YoutubeUrl = (string?)null,
            GameUrl = (string?)null,
            MinPlayers = 4,
            MaxPlayers = 2
        });
        Assert.Equal(HttpStatusCode.BadRequest, badPlayers.StatusCode);
    }

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

        await factory.WithDbContextAsync(async db =>
        {
            var p = await db.Players.FirstAsync();
            p.HasJoker = true;
            p.CurrentPhase = Domain.Phase.Vote;
            var o = await db.Players.SingleAsync(x => x.Username == "other");
            o.VotesFinal = true;
            await db.SaveChangesAsync();
        });

        var suggestion = await player.PostAsJsonAsync("/api/suggestions", new
        {
            Name = "JokerGame",
            Genre = (string?)null,
            Description = (string?)null,
            ScreenshotUrl = (string?)null,
            YoutubeUrl = (string?)null,
            GameUrl = (string?)null,
            MinPlayers = (int?)null,
            MaxPlayers = (int?)null
        });
        suggestion.EnsureSuccessStatusCode();

        await factory.WithDbContextAsync(async db =>
        {
            var p = await db.Players.FirstAsync();
            Assert.False(p.HasJoker);
            Assert.False(p.VotesFinal);
            var o = await db.Players.SingleAsync(x => x.Username == "other");
            Assert.False(o.VotesFinal);
        });
    }

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

        var player = factory.CreateClientWithCookies();
        await player.RegisterAsync("author");
        var id = await player.CreateSuggestionAsync("OldName");
        await player.PostAsJsonAsync("/api/me/phase/next", new { });

        var update = await admin.PutAsJsonAsync($"/api/suggestions/{id}", new
        {
            Name = "NewName",
            Genre = (string?)null,
            Description = (string?)null,
            ScreenshotUrl = (string?)null,
            YoutubeUrl = (string?)null,
            GameUrl = (string?)null,
            MinPlayers = (int?)null,
            MaxPlayers = (int?)null
        });
        update.EnsureSuccessStatusCode();
    }

    [Fact]
    public async Task Phase_gate_blocks_player_update_in_vote_phase()
    {
        await using var factory = new TestWebApplicationFactory();
        var player = factory.CreateClientWithCookies();
        await player.RegisterAsync("phase");
        var id = await player.CreateSuggestionAsync("Lock");
        await player.PostAsJsonAsync("/api/me/phase/next", new { });

        var update = await player.PutAsJsonAsync($"/api/suggestions/{id}", new
        {
            Name = "Blocked",
            Genre = "NewGenre",
            Description = (string?)null,
            ScreenshotUrl = (string?)null,
            YoutubeUrl = (string?)null,
            GameUrl = (string?)null,
            MinPlayers = (int?)null,
            MaxPlayers = (int?)null
        });

        update.EnsureSuccessStatusCode();

        var loaded = await factory.WithDbContextAsync(async db => await db.Suggestions.FindAsync(id));
        Assert.Equal("Lock", loaded!.Name); // title locked
        Assert.Equal("NewGenre", loaded.Genre); // other fields still editable in vote
    }

    [Fact]
    public async Task Player_cannot_edit_suggestion_in_results_phase()
    {
        await using var factory = new TestWebApplicationFactory();
        var player = factory.CreateClientWithCookies();
        await player.RegisterAsync("results");
        var id = await player.CreateSuggestionAsync("Frozen");

        // Move everyone to Results
        await factory.WithDbContextAsync(async db =>
        {
            var state = await db.AppState.FirstAsync();
            state.ResultsOpen = true;
            var p = await db.Players.FirstAsync();
            p.CurrentPhase = Domain.Phase.Results;
            await db.SaveChangesAsync();
        });

        var update = await player.PutAsJsonAsync($"/api/suggestions/{id}", new
        {
            Name = "ShouldFail",
            Genre = "Nope",
            Description = (string?)null,
            ScreenshotUrl = (string?)null,
            YoutubeUrl = (string?)null,
            GameUrl = (string?)null,
            MinPlayers = (int?)null,
            MaxPlayers = (int?)null
        });

        Assert.Equal(HttpStatusCode.BadRequest, update.StatusCode);

        var loaded = await factory.WithDbContextAsync(async db => await db.Suggestions.FindAsync(id));
        Assert.Equal("Frozen", loaded!.Name);
        Assert.Equal("Coop", loaded.Genre);
    }

    [Fact]
    public async Task Player_cannot_edit_other_players_suggestion()
    {
        await using var factory = new TestWebApplicationFactory();
        var owner = factory.CreateClientWithCookies();
        await owner.RegisterAsync("owner");
        var other = factory.CreateClientWithCookies();
        await other.RegisterAsync("intruder");

        var id = await owner.CreateSuggestionAsync("Protected");

        var update = await other.PutAsJsonAsync($"/api/suggestions/{id}", new
        {
            Name = "Hacked",
            Genre = "Bad",
            Description = (string?)null,
            ScreenshotUrl = (string?)null,
            YoutubeUrl = (string?)null,
            GameUrl = (string?)null,
            MinPlayers = (int?)null,
            MaxPlayers = (int?)null
        });

        Assert.Equal(HttpStatusCode.Unauthorized, update.StatusCode);
    }

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

        // Seed 5 suggestions in Suggest phase
        for (var i = 0; i < 5; i++)
        {
            var resp = await client.PostAsJsonAsync("/api/suggestions", new
            {
                Name = $"Game{i}",
                Genre = (string?)null,
                Description = (string?)null,
                ScreenshotUrl = (string?)null,
                YoutubeUrl = (string?)null,
                GameUrl = (string?)null,
                MinPlayers = (int?)null,
                MaxPlayers = (int?)null
            });
            resp.EnsureSuccessStatusCode();
        }

        // Move to Vote and grant joker
        await client.PostAsJsonAsync("/api/me/phase/next", new { });
        await factory.WithDbContextAsync(async db =>
        {
            var p = await db.Players.FirstAsync();
            p.HasJoker = true;
            await db.SaveChangesAsync();
        });

        var sixth = await client.PostAsJsonAsync("/api/suggestions", new
        {
            Name = "JokerExtra",
            Genre = (string?)null,
            Description = (string?)null,
            ScreenshotUrl = (string?)null,
            YoutubeUrl = (string?)null,
            GameUrl = (string?)null,
            MinPlayers = (int?)null,
            MaxPlayers = (int?)null
        });
        sixth.EnsureSuccessStatusCode();

        // Grant another joker and add a seventh suggestion
        await factory.WithDbContextAsync(async db =>
        {
            var p = await db.Players.FirstAsync();
            p.HasJoker = true;
            await db.SaveChangesAsync();
        });

        var seventh = await client.PostAsJsonAsync("/api/suggestions", new
        {
            Name = "TooMany",
            Genre = (string?)null,
            Description = (string?)null,
            ScreenshotUrl = (string?)null,
            YoutubeUrl = (string?)null,
            GameUrl = (string?)null,
            MinPlayers = (int?)null,
            MaxPlayers = (int?)null
        });

        seventh.EnsureSuccessStatusCode();

        // No joker left; further suggestions should be blocked
        var eighth = await client.PostAsJsonAsync("/api/suggestions", new
        {
            Name = "BlockedNow",
            Genre = (string?)null,
            Description = (string?)null,
            ScreenshotUrl = (string?)null,
            YoutubeUrl = (string?)null,
            GameUrl = (string?)null,
            MinPlayers = (int?)null,
            MaxPlayers = (int?)null
        });

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

    [Fact]
    public async Task Unreachable_screenshot_url_is_rejected()
    {
        await using var factory = new TestWebApplicationFactory();
        factory.HttpHandler.SetResponder(_ => new HttpResponseMessage(HttpStatusCode.BadRequest));

        var client = factory.CreateClientWithCookies();
        await client.RegisterAsync("imgtester");

        var response = await client.PostAsJsonAsync("/api/suggestions", new
        {
            Name = "Needs image",
            Genre = (string?)null,
            Description = (string?)null,
            ScreenshotUrl = "http://example.com/image.png",
            YoutubeUrl = (string?)null,
            GameUrl = (string?)null,
            MinPlayers = (int?)null,
            MaxPlayers = (int?)null
        });

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

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

        var resp = await client.GetAsync("/api/suggestions/all");
        Assert.Equal(HttpStatusCode.BadRequest, resp.StatusCode);
    }

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

        await client.PostAsJsonAsync("/api/suggestions", new
        {
            Name = "Second",
            Genre = (string?)null,
            Description = (string?)null,
            ScreenshotUrl = (string?)null,
            YoutubeUrl = (string?)null,
            GameUrl = (string?)null,
            MinPlayers = (int?)null,
            MaxPlayers = (int?)null
        });
        await Task.Delay(10);
        await client.PostAsJsonAsync("/api/suggestions", new
        {
            Name = "Third",
            Genre = (string?)null,
            Description = (string?)null,
            ScreenshotUrl = (string?)null,
            YoutubeUrl = (string?)null,
            GameUrl = (string?)null,
            MinPlayers = (int?)null,
            MaxPlayers = (int?)null
        });

        var mine = await client.GetFromJsonAsync<List<JsonElement>>("/api/suggestions/mine");
        Assert.Equal("Second", mine![0].GetProperty("name").GetString());
    }

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

        await factory.WithDbContextAsync(async db =>
        {
            var p = await db.Players.FirstAsync();
            p.CurrentPhase = Domain.Phase.Vote;
            p.DisplayName = null;
            await db.SaveChangesAsync();
        });

        var badPhase = await client.PostAsJsonAsync("/api/suggestions", new
        {
            Name = "Nope",
            Genre = (string?)null,
            Description = (string?)null,
            ScreenshotUrl = (string?)null,
            YoutubeUrl = (string?)null,
            GameUrl = (string?)null,
            MinPlayers = (int?)null,
            MaxPlayers = (int?)null
        });
        Assert.Equal(HttpStatusCode.BadRequest, badPhase.StatusCode);

        await factory.WithDbContextAsync(async db =>
        {
            var p = await db.Players.FirstAsync();
            p.CurrentPhase = Domain.Phase.Suggest;
            await db.SaveChangesAsync();
        });

        var noDisplay = await client.PostAsJsonAsync("/api/suggestions", new
        {
            Name = "NoDisplay",
            Genre = (string?)null,
            Description = (string?)null,
            ScreenshotUrl = (string?)null,
            YoutubeUrl = (string?)null,
            GameUrl = (string?)null,
            MinPlayers = (int?)null,
            MaxPlayers = (int?)null
        });
        Assert.Equal(HttpStatusCode.BadRequest, noDisplay.StatusCode);
    }

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

        var badGame = await client.PostAsJsonAsync("/api/suggestions", new
        {
            Name = "Bad",
            Genre = (string?)null,
            Description = (string?)null,
            ScreenshotUrl = (string?)null,
            YoutubeUrl = (string?)null,
            GameUrl = "ftp://bad",
            MinPlayers = (int?)null,
            MaxPlayers = (int?)null
        });
        Assert.Equal(HttpStatusCode.BadRequest, badGame.StatusCode);

        var badYoutube = await client.PostAsJsonAsync("/api/suggestions", new
        {
            Name = "BadYt",
            Genre = (string?)null,
            Description = (string?)null,
            ScreenshotUrl = (string?)null,
            YoutubeUrl = "file://bad",
            GameUrl = (string?)null,
            MinPlayers = (int?)null,
            MaxPlayers = (int?)null
        });
        Assert.Equal(HttpStatusCode.BadRequest, badYoutube.StatusCode);

        var longName = await client.PostAsJsonAsync("/api/suggestions", new
        {
            Name = new string('x', 101),
            Genre = (string?)null,
            Description = (string?)null,
            ScreenshotUrl = (string?)null,
            YoutubeUrl = (string?)null,
            GameUrl = (string?)null,
            MinPlayers = (int?)null,
            MaxPlayers = (int?)null
        });
        Assert.Equal(HttpStatusCode.BadRequest, longName.StatusCode);

        var minOnly = await client.PostAsJsonAsync("/api/suggestions", new
        {
            Name = "MinOnly",
            Genre = (string?)null,
            Description = (string?)null,
            ScreenshotUrl = (string?)null,
            YoutubeUrl = (string?)null,
            GameUrl = (string?)null,
            MinPlayers = 2,
            MaxPlayers = (int?)null
        });
        Assert.Equal(HttpStatusCode.BadRequest, minOnly.StatusCode);

        var maxTooHigh = await client.PostAsJsonAsync("/api/suggestions", new
        {
            Name = "MaxHigh",
            Genre = (string?)null,
            Description = (string?)null,
            ScreenshotUrl = (string?)null,
            YoutubeUrl = (string?)null,
            GameUrl = (string?)null,
            MinPlayers = 2,
            MaxPlayers = 40
        });
        Assert.Equal(HttpStatusCode.BadRequest, maxTooHigh.StatusCode);
    }

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

        var longGenre = new string('g', 60);
        var longDesc = new string('d', 600);
        var resp = await client.PostAsJsonAsync("/api/suggestions", new
        {
            Name = "Trim",
            Genre = $"  {longGenre}  ",
            Description = $"  {longDesc}  ",
            ScreenshotUrl = "http://example.com/img.png",
            YoutubeUrl = "http://example.com/y",
            GameUrl = "http://example.com/g",
            MinPlayers = 1,
            MaxPlayers = 4
        });
        resp.EnsureSuccessStatusCode();

        await factory.WithDbContextAsync(async db =>
        {
            var s = await db.Suggestions.AsNoTracking().FirstAsync();
            Assert.Equal(50, s.Genre!.Length);
            Assert.Equal(500, s.Description!.Length);
            Assert.Equal("http://example.com/img.png", s.ScreenshotUrl);
        });
    }

    [Fact]
    public async Task Mine_excludes_other_players()
    {
        await using var factory = new TestWebApplicationFactory();
        var a = factory.CreateClientWithCookies();
        await a.RegisterAsync("alice");
        var b = factory.CreateClientWithCookies();
        await b.RegisterAsync("bob");

        await a.CreateSuggestionAsync("AliceGame");
        await b.CreateSuggestionAsync("BobGame");

        var mine = await a.GetFromJsonAsync<List<JsonElement>>("/api/suggestions/mine");
        Assert.NotNull(mine);
        Assert.Single(mine);
        Assert.Equal("AliceGame", mine[0].GetProperty("name").GetString());
    }

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

        var id1 = await client.CreateSuggestionAsync("Alpha");
        await Task.Delay(10);
        var id2 = await client.CreateSuggestionAsync("Beta");
        await factory.WithDbContextAsync(async db =>
        {
            var beta = await db.Suggestions.FindAsync(id2);
            beta!.ParentSuggestionId = id1;
            await db.SaveChangesAsync();
        });

        await client.PostAsJsonAsync("/api/me/phase/next", new { }); // to Vote

        var all = await client.GetFromJsonAsync<List<JsonElement>>("/api/suggestions/all");
        Assert.Equal(2, all!.Count);
        var first = all[0];
        Assert.Equal("Alpha", first.GetProperty("name").GetString());
        var second = all[1];
        var linkedIds = second.GetProperty("linkedIds").EnumerateArray().Select(x => x.GetInt32()).ToList();
        Assert.Contains(id1, linkedIds);
        var linkedTitles = second.GetProperty("linkedTitles").EnumerateArray().Select(x => x.GetString()).ToList();
        Assert.Contains("Alpha", linkedTitles);
    }

    [Fact]
    public async Task Delete_respects_phase_and_clears_links_and_votes()
    {
        await using var factory = new TestWebApplicationFactory();
        var owner = factory.CreateClientWithCookies();
        await owner.RegisterAsync("deleter");
        var other = factory.CreateClientWithCookies();
        await other.RegisterAsync("voter");

        var id = await owner.CreateSuggestionAsync("DeleteMe");
        var child = await owner.CreateSuggestionAsync("Child");
        await factory.WithDbContextAsync(async db =>
        {
            var c = await db.Suggestions.FindAsync(child);
            c!.ParentSuggestionId = id;
            await db.SaveChangesAsync();
        });

        await owner.PostAsJsonAsync("/api/me/phase/next", new { }); // Vote
        await other.PostAsJsonAsync("/api/me/phase/next", new { });
        await other.PostAsJsonAsync("/api/votes", new
        {
            SuggestionId = id,
            Score = 5
        });

        var blocked = await owner.DeleteAsync($"/api/suggestions/{id}");
        Assert.Equal(HttpStatusCode.BadRequest, blocked.StatusCode);

        var admin = factory.CreateClientWithCookies();
        await admin.RegisterAsync("admin", admin: true);
        var delete = await admin.DeleteAsync($"/api/suggestions/{id}");
        delete.EnsureSuccessStatusCode();

        await factory.WithDbContextAsync(async db =>
        {
            Assert.False(await db.Suggestions.AnyAsync(s => s.Id == id));
            var childEntity = await db.Suggestions.FindAsync(child);
            Assert.Null(childEntity!.ParentSuggestionId);
            Assert.False(db.Votes.Any(v => v.SuggestionId == id));
        });
    }
}
