using MBN_STOCK_WEBVIEW.Core.Playout;

namespace MBN_STOCK_WEBVIEW.Core.Tests;

public sealed class ScenePagingTests
{
    public static TheoryData<ScenePageSize, int> SupportedPageSizes => new()
    {
        { ScenePageSize.Five, 5 },
        { ScenePageSize.Six, 6 },
        { ScenePageSize.Twelve, 12 }
    };

    [Fact]
    public void ScenePageSize_DefinesExactlyTheLegacyPageSizes()
    {
        var values = Enum.GetValues<ScenePageSize>()
            .Select(value => (int)value)
            .OrderBy(value => value)
            .ToArray();

        Assert.Equal([5, 6, 12], values);
        Assert.Equal(20, ScenePaging.MaximumPageCount);
    }

    [Theory]
    [MemberData(nameof(SupportedPageSizes))]
    public void CreatePlan_AndWindows_CoverAllBoundaryCounts(
        ScenePageSize pageSize,
        int size)
    {
        var itemCounts = new[]
        {
            0,
            1,
            size - 1,
            size,
            size + 1,
            (20 * size) - 1,
            20 * size,
            (20 * size) + 1,
            21 * size,
            int.MaxValue
        };

        foreach (var itemCount in itemCounts)
        {
            var result = ScenePaging.CreatePlan(itemCount, pageSize);

            Assert.True(result.IsValid);
            Assert.Equal(ScenePagingResultKind.Valid, result.Kind);
            Assert.NotNull(result.Plan);
            Assert.Empty(result.Error);

            var plan = result.Plan;
            var expectedUncappedPages = itemCount == 0
                ? 0
                : ((itemCount - 1) / size) + 1;
            var expectedTotalPages = Math.Min(20, expectedUncappedPages);
            var expectedAccessibleItems = Math.Min(itemCount, 20 * size);

            Assert.Equal(itemCount, plan.ItemCount);
            Assert.Equal(pageSize, plan.PageSize);
            Assert.Equal(expectedTotalPages, plan.TotalPages);
            Assert.Equal(expectedAccessibleItems, plan.AccessibleItemCount);
            Assert.Equal(itemCount - expectedAccessibleItems, plan.ExcludedItemCount);

            for (var page = 1; page <= plan.TotalPages; page++)
            {
                var pageResult = ScenePaging.GetWindow(itemCount, pageSize, page);

                Assert.True(pageResult.IsValid);
                Assert.Equal(ScenePagingResultKind.Valid, pageResult.Kind);
                Assert.NotNull(pageResult.Plan);
                Assert.NotNull(pageResult.Window);
                Assert.Empty(pageResult.Error);

                var window = pageResult.Window;
                var expectedOffset = (page - 1) * size;
                var expectedCount = Math.Min(
                    size,
                    expectedAccessibleItems - expectedOffset);
                var expectedRemaining =
                    expectedAccessibleItems - expectedOffset - expectedCount;

                Assert.Equal(page, window.PageNumberOneBased);
                Assert.Equal(expectedTotalPages, window.TotalPages);
                Assert.Equal(expectedOffset, window.OffsetZeroBased);
                Assert.Equal(expectedCount, window.ItemCount);
                Assert.Equal(expectedRemaining, window.RemainingItemCount);
                Assert.InRange(window.ItemCount, 1, size);
                Assert.InRange(
                    window.OffsetZeroBased + window.ItemCount,
                    1,
                    expectedAccessibleItems);
                Assert.InRange(
                    window.RemainingItemCount,
                    0,
                    expectedAccessibleItems);
            }

            if (plan.TotalPages == 0)
            {
                var pageResult = ScenePaging.GetWindow(itemCount, pageSize, 1);

                Assert.False(pageResult.IsValid);
                Assert.Equal(ScenePagingResultKind.Invalid, pageResult.Kind);
                Assert.NotNull(pageResult.Plan);
                Assert.Null(pageResult.Window);
                Assert.NotEmpty(pageResult.Error);
            }
        }
    }

    [Theory]
    [MemberData(nameof(SupportedPageSizes))]
    public void GetWindow_PartialLastPage_UsesOneBasedPageAndZeroBasedOffset(
        ScenePageSize pageSize,
        int size)
    {
        var result = ScenePaging.GetWindow(size + 1, pageSize, 2);

        Assert.True(result.IsValid);
        Assert.NotNull(result.Window);
        Assert.Equal(2, result.Window.PageNumberOneBased);
        Assert.Equal(size, result.Window.OffsetZeroBased);
        Assert.Equal(1, result.Window.ItemCount);
        Assert.Equal(0, result.Window.RemainingItemCount);
    }

    [Theory]
    [InlineData(-1, ScenePageSize.Five)]
    [InlineData(0, (ScenePageSize)0)]
    [InlineData(1, (ScenePageSize)7)]
    [InlineData(1, (ScenePageSize)13)]
    public void CreatePlan_RejectsNegativeCountsAndUnsupportedSizes(
        int itemCount,
        ScenePageSize pageSize)
    {
        var result = ScenePaging.CreatePlan(itemCount, pageSize);

        Assert.False(result.IsValid);
        Assert.Equal(ScenePagingResultKind.Invalid, result.Kind);
        Assert.Null(result.Plan);
        Assert.NotEmpty(result.Error);
    }

    [Theory]
    [InlineData(0)]
    [InlineData(-1)]
    [InlineData(3)]
    public void GetWindow_RejectsPagesOutsideTheOneBasedRange(int pageNumberOneBased)
    {
        var result = ScenePaging.GetWindow(6, ScenePageSize.Five, pageNumberOneBased);

        Assert.False(result.IsValid);
        Assert.Equal(ScenePagingResultKind.Invalid, result.Kind);
        Assert.NotNull(result.Plan);
        Assert.Null(result.Window);
        Assert.NotEmpty(result.Error);
    }

    [Theory]
    [MemberData(nameof(SupportedPageSizes))]
    public void DecideNext_WhenCurrentPageIsBeforeTotal_ReturnsSameCueNextPage(
        ScenePageSize pageSize,
        int size)
    {
        ScenePlaylistCue[] playlist =
        [
            new("before", true),
            new("current", true, pageSize, size + 1),
            new("after", true)
        ];
        var state = new SceneNextState(playlist, 1, 1);

        var result = ScenePaging.DecideNext(state);

        Assert.True(result.IsValid);
        Assert.Equal(SceneNextDecisionKind.SameCueNextPage, result.Kind);
        Assert.Equal(1, result.TargetCueIndexZeroBased);
        Assert.Equal(2, result.TargetPageNumberOneBased);
        Assert.NotNull(result.TargetPage);
        Assert.Equal(size, result.TargetPage.OffsetZeroBased);
        Assert.Equal(1, result.TargetPage.ItemCount);
        Assert.Equal(0, result.TargetPage.RemainingItemCount);
        Assert.Empty(result.Error);
    }

    [Fact]
    public void DecideNext_OnLastPage_SelectsFirstEnabledCueAfterCurrent()
    {
        ScenePlaylistCue[] playlist =
        [
            new("enabled-before", true),
            new("current", true, ScenePageSize.Five, 6),
            new("disabled-after", false),
            new("first-enabled-after", true, ScenePageSize.Six, 7),
            new("later-enabled", true)
        ];
        var state = new SceneNextState(playlist, 1, 2);

        var result = ScenePaging.DecideNext(state);

        Assert.True(result.IsValid);
        Assert.Equal(SceneNextDecisionKind.NextEnabledCue, result.Kind);
        Assert.Equal(3, result.TargetCueIndexZeroBased);
        Assert.Equal(1, result.TargetPageNumberOneBased);
        Assert.NotNull(result.TargetPage);
        Assert.Equal(0, result.TargetPage.OffsetZeroBased);
        Assert.Equal(6, result.TargetPage.ItemCount);
        Assert.Equal(1, result.TargetPage.RemainingItemCount);
        Assert.Empty(result.Error);
    }

    [Fact]
    public void DecideNext_ForNonPagedCue_SelectsFirstEnabledCueAfterCurrent()
    {
        ScenePlaylistCue[] playlist =
        [
            new("current", true),
            new("disabled", false),
            new("next", true)
        ];

        var result = ScenePaging.DecideNext(new SceneNextState(playlist, 0, null));

        Assert.Equal(SceneNextDecisionKind.NextEnabledCue, result.Kind);
        Assert.Equal(2, result.TargetCueIndexZeroBased);
        Assert.Null(result.TargetPageNumberOneBased);
        Assert.Null(result.TargetPage);
    }

    [Fact]
    public void DecideNext_ForEmptyPagedCue_SelectsNextCueWithoutInventingPageZero()
    {
        ScenePlaylistCue[] playlist =
        [
            new("empty-current", true, ScenePageSize.Twelve, 0),
            new("empty-next", true, ScenePageSize.Five, 0)
        ];

        var result = ScenePaging.DecideNext(new SceneNextState(playlist, 0, null));

        Assert.Equal(SceneNextDecisionKind.NextEnabledCue, result.Kind);
        Assert.Equal(1, result.TargetCueIndexZeroBased);
        Assert.Null(result.TargetPageNumberOneBased);
        Assert.Null(result.TargetPage);
    }

    [Fact]
    public void DecideNext_DoesNotWrapToAnEnabledCueBeforeCurrent()
    {
        ScenePlaylistCue[] playlist =
        [
            new("enabled-before", true),
            new("current", true, ScenePageSize.Five, 1),
            new("disabled-after", false)
        ];

        var result = ScenePaging.DecideNext(new SceneNextState(playlist, 1, 1));

        Assert.True(result.IsValid);
        Assert.Equal(SceneNextDecisionKind.EndOfPlaylist, result.Kind);
        Assert.Null(result.TargetCueIndexZeroBased);
        Assert.Null(result.TargetPageNumberOneBased);
        Assert.Null(result.TargetPage);
        Assert.Empty(result.Error);
    }

    [Theory]
    [InlineData("null-state")]
    [InlineData("null-playlist")]
    [InlineData("empty-playlist")]
    [InlineData("negative-index")]
    [InlineData("index-at-count")]
    [InlineData("blank-code")]
    [InlineData("non-paged-items")]
    [InlineData("non-paged-current-page")]
    [InlineData("negative-items")]
    [InlineData("unsupported-page-size")]
    [InlineData("missing-current-page")]
    [InlineData("page-zero")]
    [InlineData("page-after-total")]
    [InlineData("empty-cue-with-page")]
    [InlineData("invalid-future-cue")]
    public void DecideNext_RejectsInvalidState(string scenario)
    {
        var validPagedCue = new ScenePlaylistCue(
            "current",
            true,
            ScenePageSize.Five,
            6);
        SceneNextState? state = scenario switch
        {
            "null-state" => null,
            "null-playlist" => new SceneNextState(null, 0, null),
            "empty-playlist" => new SceneNextState([], 0, null),
            "negative-index" => new SceneNextState([new("cue", true)], -1, null),
            "index-at-count" => new SceneNextState([new("cue", true)], 1, null),
            "blank-code" => new SceneNextState([new(" ", true)], 0, null),
            "non-paged-items" => new SceneNextState([new("cue", true, null, 1)], 0, null),
            "non-paged-current-page" => new SceneNextState([new("cue", true)], 0, 1),
            "negative-items" => new SceneNextState(
                [new("cue", true, ScenePageSize.Five, -1)],
                0,
                null),
            "unsupported-page-size" => new SceneNextState(
                [new("cue", true, (ScenePageSize)7, 1)],
                0,
                1),
            "missing-current-page" => new SceneNextState([validPagedCue], 0, null),
            "page-zero" => new SceneNextState([validPagedCue], 0, 0),
            "page-after-total" => new SceneNextState([validPagedCue], 0, 3),
            "empty-cue-with-page" => new SceneNextState(
                [new("cue", true, ScenePageSize.Five, 0)],
                0,
                1),
            "invalid-future-cue" => new SceneNextState(
                [new("current", true), new("next", true, null, 1)],
                0,
                null),
            _ => throw new InvalidOperationException($"Unknown scenario: {scenario}")
        };

        var result = ScenePaging.DecideNext(state);

        Assert.False(result.IsValid);
        Assert.Equal(SceneNextDecisionKind.Invalid, result.Kind);
        Assert.Null(result.TargetCueIndexZeroBased);
        Assert.Null(result.TargetPageNumberOneBased);
        Assert.Null(result.TargetPage);
        Assert.NotEmpty(result.Error);
    }
}
