using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using MBN_STOCK_WEBVIEW.Core.Playout;
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;

namespace MBN_STOCK_WEBVIEW.Core.Tests;

public sealed class PagedQuoteSceneBuildersTests
{
    public static TheoryData<HeaderCase> HeaderCases => new()
    {
        new(
            PagedQuoteBranch.Domestic,
            PagedQuoteHeaderStyle.StandardWon,
            PagedQuoteTitleStyle.Exact,
            NxtMarketSession.NotApplicable,
            "Domestic",
            "Domestic",
            "\uB2E8\uC704 : \uC6D0",
            "\uD604\uC7AC\uAC00",
            null),
        new(
            PagedQuoteBranch.Nxt,
            PagedQuoteHeaderStyle.StandardWon,
            PagedQuoteTitleStyle.Exact,
            NxtMarketSession.PreMarket,
            "NXT",
            "NXT",
            "\uB2E8\uC704 : \uC6D0",
            "\uD604\uC7AC\uAC00",
            "Images\\\uD504\uB9AC\uB9C8\uCF13.png"),
        new(
            PagedQuoteBranch.NxtTheme,
            PagedQuoteHeaderStyle.StandardWon,
            PagedQuoteTitleStyle.ExpectedExecution,
            NxtMarketSession.AfterMarket,
            "Theme",
            "Theme \uC608\uC0C1 \uCCB4\uACB0\uAC00",
            "\uB2E8\uC704 : \uC6D0",
            "\uD604\uC7AC\uAC00",
            "Images\\\uC560\uD504\uD130\uB9C8\uCF13.png"),
        new(
            PagedQuoteBranch.Theme,
            PagedQuoteHeaderStyle.StandardWon,
            PagedQuoteTitleStyle.Exact,
            NxtMarketSession.NotApplicable,
            "Theme",
            "Theme",
            "\uB2E8\uC704 : \uC6D0",
            "\uD604\uC7AC\uAC00",
            null),
        new(
            PagedQuoteBranch.Expert,
            PagedQuoteHeaderStyle.ExpertReturns,
            PagedQuoteTitleStyle.Exact,
            NxtMarketSession.NotApplicable,
            "Expert",
            "Expert",
            "\uB2E8\uC704 : \uC6D0",
            "\uB9E4\uC218\uAC00",
            null),
        new(
            PagedQuoteBranch.Vi,
            PagedQuoteHeaderStyle.StandardWon,
            PagedQuoteTitleStyle.Exact,
            NxtMarketSession.NotApplicable,
            "VI",
            "VI",
            "\uB2E8\uC704 : \uC6D0",
            "\uD604\uC7AC\uAC00",
            null),
        new(
            PagedQuoteBranch.Domestic,
            PagedQuoteHeaderStyle.IndustryPoints,
            PagedQuoteTitleStyle.Exact,
            NxtMarketSession.NotApplicable,
            "Industry",
            "Industry",
            "\uB2E8\uC704 : p",
            "\uD604\uC7AC",
            null)
    };

    [Fact]
    public void S5074_nxt_asset_direction_and_partial_clear_match_golden_order()
    {
        var data = Data(
            count: 1,
            pageNumberOneBased: 1,
            branch: PagedQuoteBranch.NxtTheme,
            header: PagedQuoteHeaderStyle.StandardWon,
            session: NxtMarketSession.PreMarket,
            title: "Theme");
        var actual = new S5074SceneMutationBuilder().Build(data).Select(Describe).ToArray();

        Assert.Equal(
        [
            "S:mart=0",
            "S:row1=0", "S:row2=0", "S:row3=0", "S:row4=0", "S:row5=0",
            "V:title=Theme", "A:mart=Images\\\uD504\uB9AC\uB9C8\uCF13.png", "S:mart=1",
            "V:unit=\uB2E8\uC704 : \uC6D0", "V:column1=\uD604\uC7AC\uAC00",
            "V:column2=\uB4F1\uB77D\uAC00", "V:column3=\uB4F1\uB77D\uB960",
            "S:row1=1", "V:title1=Row001", "V:price1=1,001",
            "V:changePrice1_1=-1", "V:changePrice1_2=-1", "V:changePrice1_3=-1",
            "V:rate1=-0.25",
            "S:upup1=0", "S:up1=0", "S:flat1=0", "S:down1=0", "S:downdown1=0",
            "S:bg1_1=0", "S:bg1_2=0", "S:bg1_3=1",
            "S:changePrice1_1=0", "S:changePrice1_2=0", "S:changePrice1_3=1",
            "S:down1=1"
        ], actual.Take(32));
        Assert.Equal(
        [
            "S:row2=0", "V:title2=", "V:price2=",
            "V:changePrice2_1=", "V:changePrice2_2=", "V:changePrice2_3=", "V:rate2=",
            "S:upup2=0", "S:up2=0", "S:flat2=0", "S:down2=0", "S:downdown2=0",
            "S:bg2_1=0", "S:bg2_2=0", "S:bg2_3=0",
            "S:changePrice2_1=0", "S:changePrice2_2=0", "S:changePrice2_3=0"
        ], actual.Skip(32).Take(18));
        Assert.Equal(
            "E1BC9AAD7BC6A54A871922A5C222971A94944B66099404F08F70F972AE86CED2",
            SnapshotHash(actual));
    }

    [Fact]
    public void S5077_omits_absent_global_header_and_matches_wide_direction_golden_order()
    {
        var data = Data(
            count: 6,
            pageNumberOneBased: 1,
            branch: PagedQuoteBranch.Expert,
            header: PagedQuoteHeaderStyle.ExpertReturns,
            session: NxtMarketSession.NotApplicable,
            title: "Expert");
        var actual = new S5077SceneMutationBuilder().Build(data).Select(Describe).ToArray();

        Assert.Equal(
        [
            "S:mart=0",
            "S:row1=0", "S:row2=0", "S:row3=0", "S:row4=0", "S:row5=0", "S:row6=0",
            "S:mart=0",
            "S:row1=1", "V:title1=Row001", "V:price1=1,001",
            "V:changePrice1_1=-1", "V:changePrice1_2=-1", "V:changePrice1_3=-1",
            "V:rate1_1=-0.25", "V:rate1_2=-0.25", "V:rate1_3=-0.25",
            "S:upup1=0", "S:up1=0", "S:flat1=0", "S:down1=0", "S:downdown1=0",
            "S:bg1_1=0", "S:bg1_2=0", "S:bg1_3=1",
            "S:changePrice1_1=0", "S:changePrice1_2=0", "S:changePrice1_3=1",
            "S:rate1_1=0", "S:rate1_2=0", "S:rate1_3=1",
            "S:percent1_1=0", "S:percent1_2=0", "S:percent1_3=1",
            "S:down1=1"
        ], actual.Take(35));
        Assert.Equal(
            "1B57B6FF7C5B813A7F1825B64326D917AFB10F6C92CB657205FBDCADDB759C55",
            SnapshotHash(actual));
    }

    [Fact]
    public void S5088_page_two_uses_twelve_row_offset_and_clears_remaining_slots()
    {
        var data = Data(
            count: 13,
            pageNumberOneBased: 2,
            branch: PagedQuoteBranch.Domestic,
            header: PagedQuoteHeaderStyle.StandardWon,
            session: NxtMarketSession.NotApplicable,
            title: "Twelve");
        var actual = new S5088SceneMutationBuilder().Build(data).Select(Describe).ToArray();

        Assert.Contains("V:title1=Row013", actual);
        Assert.DoesNotContain("V:title1=Row006", actual);
        Assert.Contains("V:title2=", actual);
        Assert.Equal(11, actual.Count(item =>
            item.StartsWith("V:title", StringComparison.Ordinal) && item.EndsWith('=') &&
            item != "V:title="));
        Assert.Equal(
            "7FAC4957A218E8773AD3446A4AF88775EB255DA03407D14E52F4B31A6776BBE8",
            SnapshotHash(actual));
    }

    [Fact]
    public void Builders_emit_only_global_header_objects_present_in_authoritative_assets()
    {
        var six = new S5077SceneMutationBuilder().Build(Data(6, 1));
        Assert.DoesNotContain(six, mutation =>
            mutation is PlayoutSetValue { ObjectName: "title" or "unit" or
                "column1" or "column2" or "column3" });

        var twelve = new S5088SceneMutationBuilder().Build(Data(12, 1));
        Assert.Contains(twelve, mutation =>
            mutation is PlayoutSetValue { ObjectName: "title" });
        Assert.Contains(twelve, mutation =>
            mutation is PlayoutSetValue { ObjectName: "unit" });
        Assert.DoesNotContain(twelve, mutation =>
            mutation is PlayoutSetValue { ObjectName: "column1" or "column2" or "column3" });
    }

    [Theory]
    [InlineData("s5074", 5)]
    [InlineData("s5077", 6)]
    [InlineData("s5088", 12)]
    public void Builders_cover_size_boundaries_partial_pages_and_twenty_page_cap(
        string builderKey,
        int size)
    {
        var sizeMinusOne = Build(builderKey, Data(size - 1, 1));
        Assert.Contains(sizeMinusOne, mutation =>
            mutation is PlayoutSetValue { ObjectName: var name, Value: "" } &&
            name == "title" + size);

        var exactSize = Build(builderKey, Data(size, 1));
        Assert.DoesNotContain(exactSize, mutation =>
            mutation is PlayoutSetValue { ObjectName: var name, Value: "" } &&
            name.StartsWith("title", StringComparison.Ordinal));

        var sizePlusOne = Build(builderKey, Data(size + 1, 2));
        Assert.Contains(sizePlusOne, mutation =>
            mutation is PlayoutSetValue { ObjectName: "title1", Value: var value } &&
            value == $"Row{size + 1:000}");
        Assert.Contains(sizePlusOne, mutation =>
            mutation is PlayoutSetValue { ObjectName: "title2", Value: "" });

        var pageTwenty = Build(builderKey, Data(20 * size, 20));
        Assert.Contains(pageTwenty, mutation =>
            mutation is PlayoutSetValue { ObjectName: "title1", Value: var value } &&
            value == $"Row{((19 * size) + 1):000}");
        Assert.Contains(pageTwenty, mutation =>
            mutation is PlayoutSetValue { ObjectName: var name, Value: var value } &&
            name == "title" + size && value == $"Row{20 * size:000}");

        var capped = Build(builderKey, Data((20 * size) + 1, 20));
        Assert.DoesNotContain(capped, mutation =>
            mutation is PlayoutSetValue { Value: var value } &&
            value == $"Row{(20 * size) + 1:000}");
        Assert.Throws<LegacySceneDataException>(() =>
            Build(builderKey, Data((20 * size) + 1, 21)));
    }

    [Theory]
    [MemberData(nameof(HeaderCases))]
    public void Header_branches_use_closed_title_column_unit_and_asset_mapping(HeaderCase item)
    {
        var actual = new S5074SceneMutationBuilder().Build(Data(
            1,
            1,
            item.Branch,
            item.Header,
            item.Session,
            item.Title,
            item.TitleStyle));

        Assert.Contains(actual, mutation =>
            mutation is PlayoutSetValue { ObjectName: "title", Value: var value } &&
            value == item.ExpectedTitle);
        Assert.Contains(actual, mutation =>
            mutation is PlayoutSetValue { ObjectName: "unit", Value: var value } &&
            value == item.ExpectedUnit);
        Assert.Contains(actual, mutation =>
            mutation is PlayoutSetValue { ObjectName: "column1", Value: var value } &&
            value == item.ExpectedColumn1);
        if (item.ExpectedAsset is null)
        {
            Assert.DoesNotContain(actual, mutation => mutation is PlayoutSetAssetValue);
        }
        else
        {
            Assert.Contains(actual, mutation =>
                mutation is PlayoutSetAssetValue { ObjectName: "mart", AssetPath: var path } &&
                path == item.ExpectedAsset);
        }
    }

    [Fact]
    public void Presentation_contract_rejects_invalid_combinations_and_raw_sdk_input_surface()
    {
        Assert.Throws<LegacySceneDataException>(() =>
            new S5074SceneMutationBuilder().Build(Data(
                1, 1, PagedQuoteBranch.Nxt, PagedQuoteHeaderStyle.StandardWon,
                NxtMarketSession.NotApplicable, "A")));
        Assert.Throws<LegacySceneDataException>(() =>
            new S5074SceneMutationBuilder().Build(Data(
                1, 1, PagedQuoteBranch.Domestic, PagedQuoteHeaderStyle.StandardWon,
                NxtMarketSession.PreMarket, "A")));
        Assert.Throws<LegacySceneDataException>(() =>
            new S5074SceneMutationBuilder().Build(Data(
                1, 1, PagedQuoteBranch.Expert, PagedQuoteHeaderStyle.StandardWon,
                NxtMarketSession.NotApplicable, "A")));
        Assert.Throws<LegacySceneDataException>(() =>
            new S5074SceneMutationBuilder().Build(Data(
                1, 1, PagedQuoteBranch.Theme, PagedQuoteHeaderStyle.ExpertReturns,
                NxtMarketSession.NotApplicable, "A")));

        var publicProperties = typeof(PagedQuoteSceneData)
            .GetProperties(BindingFlags.Instance | BindingFlags.Public)
            .Select(property => property.Name);
        Assert.DoesNotContain(publicProperties, name =>
            name is "ObjectName" or "MethodName" or "AssetPath" or "SceneFile");
    }

    private static PagedQuoteSceneData Data(
        int count,
        int pageNumberOneBased,
        PagedQuoteBranch branch = PagedQuoteBranch.Domestic,
        PagedQuoteHeaderStyle header = PagedQuoteHeaderStyle.StandardWon,
        NxtMarketSession session = NxtMarketSession.NotApplicable,
        string title = "Title",
        PagedQuoteTitleStyle titleStyle = PagedQuoteTitleStyle.Exact)
    {
        var directions = new[]
        {
            ScenePriceDirection.Down,
            ScenePriceDirection.Up,
            ScenePriceDirection.Flat,
            ScenePriceDirection.LimitDown,
            ScenePriceDirection.LimitUp
        };
        var rows = Enumerable.Range(1, count)
            .Select(index => new PagedQuoteRowData(
                $"Row{index:000}",
                1000 + index,
                index % 2 == 0 ? index : -index,
                index % 2 == 0 ? index / 4d : -index / 4d,
                directions[(index - 1) % directions.Length]))
            .ToArray();
        return new PagedQuoteSceneData(
            branch,
            header,
            titleStyle,
            session,
            title,
            rows,
            pageNumberOneBased);
    }

    private static IReadOnlyList<PlayoutMutation> Build(
        string builderKey,
        PagedQuoteSceneData data) => builderKey switch
    {
        "s5074" => new S5074SceneMutationBuilder().Build(data),
        "s5077" => new S5077SceneMutationBuilder().Build(data),
        "s5088" => new S5088SceneMutationBuilder().Build(data),
        _ => throw new InvalidOperationException("Unknown test builder.")
    };

    private static string SnapshotHash(IEnumerable<string> snapshot)
    {
        var bytes = Encoding.UTF8.GetBytes(string.Join('\n', snapshot));
        return Convert.ToHexString(SHA256.HashData(bytes));
    }

    private static string Describe(PlayoutMutation mutation) => mutation switch
    {
        PlayoutSetValue value => $"V:{value.ObjectName}={value.Value}",
        PlayoutSetAssetValue asset => $"A:{asset.ObjectName}={asset.AssetPath}",
        PlayoutSetVisible visible => $"S:{visible.ObjectName}={(visible.IsVisible ? 1 : 0)}",
        _ => mutation.GetType().Name
    };

    public sealed record HeaderCase(
        PagedQuoteBranch Branch,
        PagedQuoteHeaderStyle Header,
        PagedQuoteTitleStyle TitleStyle,
        NxtMarketSession Session,
        string Title,
        string ExpectedTitle,
        string ExpectedUnit,
        string ExpectedColumn1,
        string? ExpectedAsset);
}
