using MMoneyCoderSharp.Data;

namespace MBN_STOCK_WEBVIEW.LegacyApplication.Tests;

public sealed class LegacyStockCutInteractionParityTests
{
    [Fact]
    public async Task ThreeOrderedProgramClicksSelectThenAppendExactlyOneSafeTailRow()
    {
        var controller = new LegacyOperatorController(new QueryLookup());
        await controller.SearchStocksAsync("first", LegacySearchTrigger.Button);
        controller.SelectStock(0);
        controller.CutPointerDown(
            1,
            control: false,
            shift: false,
            x: 2,
            y: 24,
            timestampMilliseconds: 1_000);

        controller.CutPointerDown(
            0,
            control: false,
            shift: false,
            x: 2,
            y: 1,
            timestampMilliseconds: 10_000);
        controller.CutPointerDown(
            0,
            control: false,
            shift: false,
            x: 2,
            y: 1,
            timestampMilliseconds: 10_046);
        var state = controller.CutPointerDown(
            0,
            control: false,
            shift: false,
            x: 2,
            y: 1,
            timestampMilliseconds: 10_089);

        var appended = Assert.Single(state.Playlist);
        Assert.Equal(LegacyStockCutCatalog.Rows[0].RawLabel, appended.RawCutLabel);
        Assert.Equal(
            [0],
            state.CutRows.Where(row => row.IsSelected).Select(row => row.PhysicalIndex));
    }

    [Fact]
    public async Task ASecondDragInsideTheLegacyDoubleClickWindowAddsOnceOnPointerDownAndOnceOnDrop()
    {
        var controller = new LegacyOperatorController(new QueryLookup());
        await controller.SearchStocksAsync("first", LegacySearchTrigger.Button);
        controller.SelectStock(0);

        controller.CutPointerDown(1, false, false, 2, 24, 1_000);
        controller.DropSelectedCutsOnPlaylist();
        controller.CutPointerDown(1, false, false, 2, 24, 1_313);
        var state = controller.DropSelectedCutsOnPlaylist();

        Assert.Equal(3, state.Playlist.Count);
        Assert.All(
            state.Playlist,
            row => Assert.Equal(LegacyStockCutCatalog.Rows[1].RawLabel, row.RawCutLabel));
    }

    [Fact]
    public async Task ASecondDragAtTheExactLegacyBoundaryAddsOnlyItsDrop()
    {
        var controller = new LegacyOperatorController(new QueryLookup());
        await controller.SearchStocksAsync("first", LegacySearchTrigger.Button);
        controller.SelectStock(0);

        controller.CutPointerDown(1, false, false, 2, 24, 1_000);
        controller.DropSelectedCutsOnPlaylist();
        controller.CutPointerDown(1, false, false, 2, 24, 1_500);
        var state = controller.DropSelectedCutsOnPlaylist();

        Assert.Equal(2, state.Playlist.Count);
        Assert.All(
            state.Playlist,
            row => Assert.Equal(LegacyStockCutCatalog.Rows[1].RawLabel, row.RawCutLabel));
    }

    [Fact]
    public async Task DropPreservesExistingPrefixAndAppendsSelectedCutsAtTheTail()
    {
        var controller = new LegacyOperatorController(new QueryLookup());
        await controller.SearchStocksAsync("first", LegacySearchTrigger.Button);
        controller.SelectStock(0);

        controller.CutPointerDown(0, false, false, 2, 1, 1_000);
        var initial = controller.CutPointerDown(0, false, false, 2, 1, 1_100);
        var existing = Assert.Single(initial.Playlist);

        controller.CutPointerDown(1, false, false, 2, 24, 2_000);
        controller.CutPointerDown(2, true, false, 2, 47, 2_100);
        var dropped = controller.DropSelectedCutsOnPlaylist();

        Assert.Equal(3, dropped.Playlist.Count);
        Assert.Equal(existing, dropped.Playlist[0]);
        Assert.Equal(
            [
                LegacyStockCutCatalog.Rows[1].RawLabel,
                LegacyStockCutCatalog.Rows[2].RawLabel
            ],
            dropped.Playlist.Skip(1).Select(row => row.RawCutLabel));
    }

    [Fact]
    public async Task EmptySearchButtonPreservesResultsStockAndCutSelection()
    {
        var lookup = new QueryLookup();
        var controller = new LegacyOperatorController(lookup);
        await controller.SearchStocksAsync("first", LegacySearchTrigger.Button);
        controller.SelectStock(0);
        controller.CutPointerDown(1, control: false, shift: false, x: 2, y: 24,
            timestampMilliseconds: 1_000);

        var state = await controller.SearchStocksAsync(
            string.Empty,
            LegacySearchTrigger.Button);

        Assert.Equal(1, lookup.CallCount);
        Assert.Equal(string.Empty, state.SearchText);
        Assert.Equal("First stock", Assert.Single(state.SearchResults).DisplayName);
        Assert.Equal(0, state.SelectedStockIndex);
        Assert.Equal(
            [1],
            state.CutRows.Where(row => row.IsSelected).Select(row => row.PhysicalIndex));
        Assert.Empty(state.Playlist);
    }

    [Fact]
    public async Task EmptyEnterQueriesAndClearsOnlyTheStockResultSelection()
    {
        var lookup = new QueryLookup();
        var controller = new LegacyOperatorController(lookup);
        await controller.SearchStocksAsync("first", LegacySearchTrigger.Button);
        controller.SelectStock(0);
        controller.CutPointerDown(1, control: false, shift: false, x: 2, y: 24,
            timestampMilliseconds: 1_000);

        var state = await controller.SearchStocksAsync(
            string.Empty,
            LegacySearchTrigger.Enter);

        Assert.Equal(2, lookup.CallCount);
        Assert.Null(state.SelectedStockIndex);
        Assert.Equal("All stock", Assert.Single(state.SearchResults).DisplayName);
        Assert.Equal(
            [1],
            state.CutRows.Where(row => row.IsSelected).Select(row => row.PhysicalIndex));
    }

    [Fact]
    public async Task RetainedCutSelectionUsesTheNewlySelectedStockAtDropTime()
    {
        var lookup = new QueryLookup();
        var controller = new LegacyOperatorController(lookup);
        await controller.SearchStocksAsync("first", LegacySearchTrigger.Button);
        controller.SelectStock(0);
        controller.CutPointerDown(1, control: false, shift: false, x: 2, y: 24,
            timestampMilliseconds: 1_000);

        await controller.SearchStocksAsync("second", LegacySearchTrigger.Button);
        controller.SelectStock(0);
        var state = controller.DropSelectedCutsOnPlaylist();

        var row = Assert.Single(state.Playlist);
        Assert.Equal("Second stock", row.StockName);
        Assert.Equal("000002", row.StockCode);
        Assert.Equal(LegacyStockCutCatalog.Rows[1].RawLabel, row.RawCutLabel);
    }

    [Fact]
    public async Task OrdinaryMarketSeparatorIsIgnoredWithoutOpeningTheNxtWarning()
    {
        var controller = new LegacyOperatorController(new QueryLookup());
        await controller.SearchStocksAsync("first", LegacySearchTrigger.Button);
        controller.SelectStock(0);
        controller.CutPointerDown(3, control: false, shift: false, x: 2, y: 70,
            timestampMilliseconds: 1_000);

        var state = controller.DropSelectedCutsOnPlaylist();

        Assert.Empty(state.Playlist);
        Assert.Null(state.Dialog);
    }

    [Fact]
    public async Task InjectedRuntimeMenuIsCopiedAndDrivesViewSelectionAndDropByPhysicalOrder()
    {
        var injected = RuntimeMenu();
        var expectedFirstLabel = injected[0].RawLabel;
        var expectedLastLabel = injected[2].RawLabel;
        var controller = new LegacyOperatorController(
            new QueryLookup(),
            cutRows: injected);
        injected[0] = injected[0] with
        {
            RawLabel = LegacyStockCutCatalog.Rows[2].RawLabel
        };

        Assert.Equal(
            [expectedFirstLabel, string.Empty, expectedLastLabel],
            controller.Current.CutRows.Select(row => row.RawLabel));

        await controller.SearchStocksAsync("first", LegacySearchTrigger.Button);
        controller.SelectStock(0);
        controller.CutPointerDown(0, control: false, shift: false, x: 1, y: 1,
            timestampMilliseconds: 1_000);
        controller.CutPointerDown(2, control: true, shift: false, x: 1, y: 47,
            timestampMilliseconds: 2_000);
        var dropped = controller.DropSelectedCutsOnPlaylist();

        Assert.Equal(
            [0, 2],
            dropped.CutRows.Where(row => row.IsSelected).Select(row => row.PhysicalIndex));
        Assert.Equal(
            [expectedFirstLabel, expectedLastLabel],
            dropped.Playlist.Select(row => row.RawCutLabel));

        var revision = dropped.Revision;
        var invalidIndex = controller.CutPointerDown(
            3,
            control: false,
            shift: false,
            x: 1,
            y: 70,
            timestampMilliseconds: 3_000);
        Assert.Equal(revision, invalidIndex.Revision);
    }

    [Fact]
    public async Task InjectedRuntimeMenuPreservesTheNxtCurrentLabelRuleAfterReorder()
    {
        var menu = RuntimeMenu();
        var controller = new LegacyOperatorController(
            new NxtLookup(),
            cutRows: menu);
        await controller.SearchStocksAsync("nxt", LegacySearchTrigger.Button);
        controller.SelectStock(0);

        controller.CutPointerDown(2, control: false, shift: false, x: 1, y: 47,
            timestampMilliseconds: 1_000);
        var warning = controller.DropSelectedCutsOnPlaylist();
        Assert.Equal("NXT는 제공하지 않습니다.", warning.Dialog?.Message);
        Assert.Empty(warning.Playlist);

        controller.DismissDialog();
        controller.CutPointerDown(0, control: false, shift: false, x: 1, y: 1,
            timestampMilliseconds: 2_000);
        var accepted = controller.DropSelectedCutsOnPlaylist();
        Assert.Equal("N5001", Assert.Single(accepted.Playlist).ToPlayoutEntry().CutCode);
    }

    [Fact]
    public async Task FiftyRowRuntimeMenuCanSelectAndDropItsLastPhysicalRow()
    {
        var rows = Enumerable.Range(0, LegacyRuntimeStockCutMenuLoader.MaximumRows)
            .Select(index => new LegacyCutCatalogRow(
                index,
                index + 1,
                LegacyStockCutCatalog.Rows[1].RawLabel,
                IsSeparator: false))
            .ToArray();
        var controller = new LegacyOperatorController(new QueryLookup(), cutRows: rows);
        await controller.SearchStocksAsync("first", LegacySearchTrigger.Button);
        controller.SelectStock(0);

        controller.CutPointerDown(
            LegacyRuntimeStockCutMenuLoader.MaximumRows - 1,
            control: false,
            shift: false,
            x: 1,
            y: 1,
            timestampMilliseconds: 1_000);
        var state = controller.DropSelectedCutsOnPlaylist();

        Assert.Equal(
            LegacyRuntimeStockCutMenuLoader.MaximumRows - 1,
            Assert.Single(state.CutRows, row => row.IsSelected).PhysicalIndex);
        Assert.Equal(
            LegacyStockCutCatalog.Rows[1].RawLabel,
            Assert.Single(state.Playlist).RawCutLabel);
    }

    [Fact]
    public async Task ConsecutiveShiftGesturesKeepTheOriginalNativeListAnchor()
    {
        var controller = new LegacyOperatorController(new QueryLookup());
        await controller.SearchStocksAsync("first", LegacySearchTrigger.Button);
        controller.SelectStock(0);
        controller.CutPointerDown(0, control: false, shift: false, 1, 1, 1_000);

        var first = controller.CutPointerDown(
            2, control: false, shift: true, 1, 47, 2_000);
        var second = controller.CutPointerDown(
            4, control: false, shift: true, 1, 93, 3_000);

        Assert.Equal(
            [0, 1, 2],
            first.CutRows.Where(row => row.IsSelected).Select(row => row.PhysicalIndex));
        Assert.Equal(
            [0, 1, 2, 3, 4],
            second.CutRows.Where(row => row.IsSelected).Select(row => row.PhysicalIndex));
    }

    [Fact]
    public async Task EmptyCutListAreaClearsSelectionSoAStaleDropAddsNothing()
    {
        var controller = new LegacyOperatorController(new QueryLookup());
        await controller.SearchStocksAsync("first", LegacySearchTrigger.Button);
        controller.SelectStock(0);
        controller.CutPointerDown(1, false, false, 1, 24, 1_000);

        var cleared = controller.ClearCutSelection();
        var dropped = controller.DropSelectedCutsOnPlaylist();

        Assert.DoesNotContain(cleared.CutRows, row => row.IsSelected);
        Assert.Empty(dropped.Playlist);
    }

    [Theory]
    [InlineData(0, 2, 0, 1, 2)]
    [InlineData(3, 1, 1, 2, 3)]
    public async Task EmptyCutListAreaPreservesNativeAnchorForFollowingShift(
        int anchor,
        int target,
        params int[] expected)
    {
        var controller = new LegacyOperatorController(new QueryLookup());
        await controller.SearchStocksAsync("first", LegacySearchTrigger.Button);
        controller.SelectStock(0);
        controller.CutPointerDown(anchor, false, false, 1, 1, 1_000);
        controller.ClearCutSelection();

        var state = controller.CutPointerDown(target, false, true, 2, 2, 2_000);

        Assert.Equal(
            expected,
            state.CutRows.Where(row => row.IsSelected).Select(row => row.PhysicalIndex));
    }

    [Fact]
    public void EmptyCutListAreaIsIgnoredUntilAStockIsSelected()
    {
        var controller = new LegacyOperatorController(new QueryLookup());
        var revision = controller.Current.Revision;

        var state = controller.ClearCutSelection();

        Assert.Equal(revision, state.Revision);
    }

    [Fact]
    public async Task BlankClickResetsLegacyAnchorEvenAfterControlClearsTheLastRow()
    {
        var controller = new LegacyOperatorController(new QueryLookup());
        await controller.SearchStocksAsync("first", LegacySearchTrigger.Button);
        controller.SelectStock(0);
        controller.CutPointerDown(1, false, false, 1, 24, 1_000);
        controller.CutPointerDown(1, true, false, 1, 24, 2_000);
        controller.ClearCutSelection();

        controller.CutPointerDown(2, false, true, 2, 47, 3_000);
        var state = controller.CutPointerDown(2, false, false, 2, 47, 3_400);

        Assert.Empty(state.Playlist);
        Assert.Equal(
            [2],
            state.CutRows.Where(row => row.IsSelected).Select(row => row.PhysicalIndex));
    }

    [Theory]
    [InlineData("physical-index")]
    [InlineData("display-ordinal")]
    [InlineData("unknown-label")]
    [InlineData("separator-shape")]
    [InlineData("too-many")]
    public void InjectedRuntimeMenuRejectsInvalidAuthority(string invalidCase)
    {
        var baseline = RuntimeMenu();
        IReadOnlyList<LegacyCutCatalogRow> rows = invalidCase switch
        {
            "physical-index" =>
                [baseline[0] with { PhysicalIndex = 1 }],
            "display-ordinal" =>
                [baseline[0] with { DisplayOrdinal = 2 }],
            "unknown-label" =>
                [baseline[0] with { RawLabel = "지원하지않는_컷" }],
            "separator-shape" =>
                [new LegacyCutCatalogRow(0, 1, "-", IsSeparator: true)],
            "too-many" => Enumerable.Range(
                    0,
                    LegacyRuntimeStockCutMenuLoader.MaximumRows + 1)
                .Select(index => new LegacyCutCatalogRow(
                    index,
                    index + 1,
                    LegacyStockCutCatalog.Rows[0].RawLabel,
                    IsSeparator: false))
                .ToArray(),
            _ => throw new ArgumentOutOfRangeException(nameof(invalidCase))
        };

        Assert.Throws<ArgumentException>(() => new LegacyOperatorController(
            new QueryLookup(),
            cutRows: rows));
    }

    private static LegacyCutCatalogRow[] RuntimeMenu() =>
    [
        new LegacyCutCatalogRow(
            0,
            1,
            LegacyStockCutCatalog.Rows[1].RawLabel,
            IsSeparator: false),
        new LegacyCutCatalogRow(1, null, string.Empty, IsSeparator: true),
        new LegacyCutCatalogRow(
            2,
            2,
            LegacyStockCutCatalog.Rows[0].RawLabel,
            IsSeparator: false)
    ];

    private sealed class QueryLookup : ILegacyStockLookup
    {
        public int CallCount { get; private set; }

        public Task<LegacyStockSearchData> SearchAsync(
            string text,
            CancellationToken cancellationToken = default)
        {
            CallCount++;
            var (name, code) = text switch
            {
                "first" => ("First stock", "000001"),
                "second" => ("Second stock", "000002"),
                _ => ("All stock", "000003")
            };
            return Task.FromResult(new LegacyStockSearchData(
                text,
                [name],
                [new LegacyStockIdentity(
                    LegacyDomesticStockMarket.Kospi,
                    name,
                    code)]));
        }
    }

    private sealed class NxtLookup : ILegacyStockLookup
    {
        public Task<LegacyStockSearchData> SearchAsync(
            string text,
            CancellationToken cancellationToken = default) =>
            Task.FromResult(new LegacyStockSearchData(
                text,
                ["Next stock(NXT)"],
                [new LegacyStockIdentity(
                    LegacyDomesticStockMarket.NxtKospi,
                    "Next stock(NXT)",
                    "A00001")]));
    }
}
