using System.Text.Json;

namespace RpgRoller.Tests;

public sealed class PayloadBudgetTests
{
    [Fact]
    public void CharacterSheetPayload_StaysWithinBudget()
    {
        using var harness = ServiceTestSupport.CreateHarness();
        var service = harness.Service;

        service.Register("gm-sheet", "Password123", "GM");
        service.Register("owner-sheet", "Password123", "Owner");

        var gmSession = ServiceTestSupport.GetValue(service.Login("gm-sheet", "Password123")).SessionToken;
        var ownerSession = ServiceTestSupport.GetValue(service.Login("owner-sheet", "Password123")).SessionToken;

        var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Payload Sheet", "d6"));
        var character = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, "Payload Hero", campaign.Id));

        var groupIds = new List<Guid>
        {
            ServiceTestSupport.GetValue(service.CreateSkillGroup(ownerSession, character.Id, "Combat", "2D+1", 1, true)).Id,
            ServiceTestSupport.GetValue(service.CreateSkillGroup(ownerSession, character.Id, "Social", "2D+1", 1, true)).Id,
            ServiceTestSupport.GetValue(service.CreateSkillGroup(ownerSession, character.Id, "Knowledge", "2D+1", 1, true)).Id,
            ServiceTestSupport.GetValue(service.CreateSkillGroup(ownerSession, character.Id, "Survival", "2D+1", 1, true)).Id
        };

        for (var i = 0; i < 18; i++)
        {
            Guid? skillGroupId = i < 16 ? groupIds[i % groupIds.Count] : null;
            _ = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, character.Id, $"Skill {i:D2}", "2D+1", 1, true, skillGroupId));
        }

        var sheet = ServiceTestSupport.GetValue(service.GetCharacterSheet(ownerSession, character.Id));
        AssertPayloadWithinBudget(sheet, 12 * 1024, "initial character sheet");
    }

    [Fact]
    public void CampaignLogInitialPagePayload_StaysWithinBudget()
    {
        using var harness = ServiceTestSupport.CreateHarness(CreateScriptedRolls(220));
        var service = harness.Service;

        service.Register("gm-log-budget", "Password123", "GM");
        service.Register("owner-log-budget", "Password123", "Owner");

        var gmSession = ServiceTestSupport.GetValue(service.Login("gm-log-budget", "Password123")).SessionToken;
        var ownerSession = ServiceTestSupport.GetValue(service.Login("owner-log-budget", "Password123")).SessionToken;

        var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Payload Log", "d6"));
        var character = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, "Log Hero", campaign.Id));
        var skill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, character.Id, "Stealth", "2D+1", 1, true));

        for (var i = 0; i < 25; i++)
            _ = ServiceTestSupport.GetValue(service.RollSkill(ownerSession, skill.Id, "public"));

        var page = ServiceTestSupport.GetValue(service.GetCampaignLogPage(gmSession, campaign.Id, limit: 25));
        AssertPayloadWithinBudget(page, 8 * 1024, "initial log page");
    }

    [Fact]
    public void CampaignLogIncrementalPayload_StaysWithinBudget()
    {
        using var harness = ServiceTestSupport.CreateHarness(CreateScriptedRolls(240));
        var service = harness.Service;

        service.Register("gm-log-delta", "Password123", "GM");
        service.Register("owner-log-delta", "Password123", "Owner");

        var gmSession = ServiceTestSupport.GetValue(service.Login("gm-log-delta", "Password123")).SessionToken;
        var ownerSession = ServiceTestSupport.GetValue(service.Login("owner-log-delta", "Password123")).SessionToken;

        var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Payload Delta", "d6"));
        var character = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, "Delta Hero", campaign.Id));
        var skill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, character.Id, "Stealth", "2D+1", 1, true));

        for (var i = 0; i < 25; i++)
            _ = ServiceTestSupport.GetValue(service.RollSkill(ownerSession, skill.Id, "public"));

        var initialPage = ServiceTestSupport.GetValue(service.GetCampaignLogPage(gmSession, campaign.Id, limit: 25));
        _ = ServiceTestSupport.GetValue(service.RollSkill(ownerSession, skill.Id, "public"));

        var incrementalPage = ServiceTestSupport.GetValue(service.GetCampaignLogPage(gmSession, campaign.Id, initialPage.Cursor, 25));
        AssertPayloadWithinBudget(incrementalPage, 2 * 1024, "incremental log update");
    }

    [Fact]
    public void RolemasterCampaignLogInitialPagePayload_StaysWithinBudget()
    {
        using var harness = ServiceTestSupport.CreateHarness(CreateRolemasterOpenEndedRolls(90));
        var service = harness.Service;

        service.Register("gm-rm-log-budget", "Password123", "GM");
        service.Register("owner-rm-log-budget", "Password123", "Owner");

        var gmSession = ServiceTestSupport.GetValue(service.Login("gm-rm-log-budget", "Password123")).SessionToken;
        var ownerSession = ServiceTestSupport.GetValue(service.Login("owner-rm-log-budget", "Password123")).SessionToken;

        var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Rolemaster Payload Log", "rolemaster"));
        var character = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, "Open Hero", campaign.Id));
        var skill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, character.Id, "Awareness", "d100!+85", 0, false, null, 5));

        for (var i = 0; i < 25; i++)
            _ = ServiceTestSupport.GetValue(service.RollSkill(ownerSession, skill.Id, "public"));

        var page = ServiceTestSupport.GetValue(service.GetCampaignLogPage(gmSession, campaign.Id, limit: 25));
        AssertPayloadWithinBudget(page, 8 * 1024, "initial rolemaster log page");
    }

    [Fact]
    public void RollResultPayload_StaysWithinJsInteropBudget()
    {
        using var harness = ServiceTestSupport.CreateHarness(CreateScriptedRolls(40));
        var service = harness.Service;

        service.Register("gm-roll-budget", "Password123", "GM");
        service.Register("owner-roll-budget", "Password123", "Owner");

        var gmSession = ServiceTestSupport.GetValue(service.Login("gm-roll-budget", "Password123")).SessionToken;
        var ownerSession = ServiceTestSupport.GetValue(service.Login("owner-roll-budget", "Password123")).SessionToken;

        var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Payload Roll", "d6"));
        var character = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, "Roll Hero", campaign.Id));
        var skill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, character.Id, "Wild Roll", "6D+3", 3, true));

        var roll = ServiceTestSupport.GetValue(service.RollSkill(ownerSession, skill.Id, "public"));
        AssertPayloadWithinBudget(roll, 16 * 1024, "roll mutation response");
    }

    [Fact]
    public void RolemasterRollDetailPayload_StaysWithinBudget_AndRetryMetadataRemainsLazy()
    {
        using var harness = ServiceTestSupport.CreateHarness(66, 42);
        var service = harness.Service;

        service.Register("gm-rm-detail-budget", "Password123", "GM");
        service.Register("owner-rm-detail-budget", "Password123", "Owner");

        var gmSession = ServiceTestSupport.GetValue(service.Login("gm-rm-detail-budget", "Password123")).SessionToken;
        var ownerSession = ServiceTestSupport.GetValue(service.Login("owner-rm-detail-budget", "Password123")).SessionToken;

        var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Rolemaster Payload Detail", "rolemaster"));
        var character = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, "Detail Hero", campaign.Id));
        var skill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, character.Id, "Sense", "d100!+10", 0, false, null, 5, true));

        var roll = ServiceTestSupport.GetValue(service.RollSkill(ownerSession, skill.Id, "public"));
        var logPage = ServiceTestSupport.GetValue(service.GetCampaignLogPage(gmSession, campaign.Id, limit: 5));
        var detail = ServiceTestSupport.GetValue(service.GetRollDetail(gmSession, roll.RollId));
        Assert.Equal("66 | open-ended | retry +5", Assert.Single(logPage.Entries).SummaryText);

        AssertPayloadWithinBudget(detail, 4 * 1024, "rolemaster roll detail");

        var rollJson = JsonSerializer.Serialize(roll, SerializerOptions);
        var logPageJson = JsonSerializer.Serialize(logPage, SerializerOptions);
        var detailJson = JsonSerializer.Serialize(detail, SerializerOptions);

        Assert.DoesNotContain("\"signedContribution\":null", rollJson, StringComparison.Ordinal);
        Assert.DoesNotContain("\"signedContribution\"", logPageJson, StringComparison.Ordinal);
        Assert.DoesNotContain("\"sequence\"", logPageJson, StringComparison.Ordinal);
        Assert.DoesNotContain("\"breakdown\"", logPageJson, StringComparison.Ordinal);
        Assert.Contains("\"kind\":\"rolemaster-open-ended-initial\"", detailJson, StringComparison.Ordinal);
        Assert.Contains("\"signedContribution\":66", detailJson, StringComparison.Ordinal);
        Assert.Contains("\"attempt\":1", detailJson, StringComparison.Ordinal);
        Assert.Contains("\"attempt\":2", detailJson, StringComparison.Ordinal);
    }

    private static void AssertPayloadWithinBudget<T>(T payload, int maxBytes, string label)
    {
        var byteCount = JsonSerializer.SerializeToUtf8Bytes(payload, SerializerOptions).Length;
        Assert.True(byteCount <= maxBytes, $"{label} payload was {byteCount} bytes, above the {maxBytes}-byte budget.");
    }

    private static int[] CreateScriptedRolls(int count)
    {
        var values = new[] { 6, 5, 4, 3, 2, 1 };
        var scriptedRolls = new int[count];
        for (var i = 0; i < count; i++)
            scriptedRolls[i] = values[i % values.Length];

        return scriptedRolls;
    }

    private static int[] CreateRolemasterOpenEndedRolls(int count)
    {
        var values = new[] { 96, 100, 12 };
        var scriptedRolls = new int[count];
        for (var i = 0; i < count; i++)
            scriptedRolls[i] = values[i % values.Length];

        return scriptedRolls;
    }

    private static readonly JsonSerializerOptions SerializerOptions = RpgRollerJson.CreateSerializerOptions();
}