using System.Reflection;
using MBN_STOCK_WEBVIEW.Core.Playout;
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;

namespace MBN_STOCK_WEBVIEW.Core.Tests;

public sealed class TabularMarketSceneBuildersTests
{
    [Fact]
    public void S5023_matches_daily_and_monthly_sign_order_and_clears_empty_rows()
    {
        var data = new S5023SceneData(
            LegacyDomesticMarket.Kospi,
            [
                new DailyInvestorFlowData(
                    new DateOnly(2026, 7, 10),
                    new InvestorFlowAmounts(100, -200, 0)),
                null,
                null,
                null,
                null
            ],
            new InvestorFlowAmounts(1000, -2000, 0));

        var actual = new S5023SceneMutationBuilder().Build(data).Select(Describe);

        Assert.Equal(
        [
            "V:title=\uCF54\uC2A4\uD53C \uB9E4\uB9E4\uB3D9\uD5A5",
            "V:row1_value1=07/10",
            "C:row1_value2:171:0:0:255", "V:row1_value2=100",
            "C:row1_value3:15:99:188:255", "V:row1_value3=-200",
            "C:row1_value4:47:47:47:255", "V:row1_value4=0",
            "V:row2_value1=",
            "C:row2_value2:47:47:47:255", "V:row2_value2=",
            "C:row2_value3:47:47:47:255", "V:row2_value3=",
            "C:row2_value4:47:47:47:255", "V:row2_value4=",
            "V:row3_value1=",
            "C:row3_value2:47:47:47:255", "V:row3_value2=",
            "C:row3_value3:47:47:47:255", "V:row3_value3=",
            "C:row3_value4:47:47:47:255", "V:row3_value4=",
            "V:row4_value1=",
            "C:row4_value2:47:47:47:255", "V:row4_value2=",
            "C:row4_value3:47:47:47:255", "V:row4_value3=",
            "C:row4_value4:47:47:47:255", "V:row4_value4=",
            "V:row5_value1=",
            "C:row5_value2:47:47:47:255", "V:row5_value2=",
            "C:row5_value3:47:47:47:255", "V:row5_value3=",
            "C:row5_value4:47:47:47:255", "V:row5_value4=",
            "V:row6_value1=1,000", "C:row6_bg1:171:0:0:255",
            "V:row6_value2=-2,000", "C:row6_bg2:15:99:189:255",
            "V:row6_value3=0", "C:row6_bg3:95:95:95:255"
        ], actual);
    }

    [Fact]
    public void S5024_matches_mixed_bar_center_scale_and_zero_order()
    {
        var data = new S5024SceneData(
            LegacyDomesticMarket.Kosdaq,
            [
                new(InvestorParticipant.Individual, 100),
                new(InvestorParticipant.Foreign, -50),
                new(InvestorParticipant.Institution, 0)
            ]);

        var actual = new S5024SceneMutationBuilder().Build(data).Select(Describe);

        Assert.Equal(
        [
            "V:title=\uCF54\uC2A4\uB2E5 \uB9E4\uB9E4\uB3D9\uD5A5",
            "S:barG1_1=0", "S:barG1_2=0",
            "S:barG2_1=0", "S:barG2_2=0",
            "S:barG3_1=0", "S:barG3_2=0",
            "P:centerbar:0:-2:0:2",
            "S:barG1_1=1", "Z:bar1_1:0:0.6666667:0:2", "V:value1_1=100",
            "S:barG2_2=1", "Z:bar2_2:0:0.33333334:0:2", "V:value2_2=-50",
            "S:barG3_1=1", "Z:bar3_1:0:0:0:2", "V:value3_1=0"
        ], actual);
    }

    [Fact]
    public void S5024_empty_slots_remain_hidden_and_emit_no_invalid_scale()
    {
        var actual = new S5024SceneMutationBuilder().Build(new S5024SceneData(
            LegacyDomesticMarket.Kospi,
            []));

        Assert.Equal(8, actual.Count);
        Assert.Equal("P:centerbar:0:-150:0:2", Describe(actual[^1]));
        Assert.DoesNotContain(actual.Select(Describe), value =>
            value.Contains("NaN", StringComparison.Ordinal));
    }

    [Fact]
    public void S5025_maps_four_closed_pair_rows_and_clears_empty_slots()
    {
        var data = new S5025SceneData(
            "\uAC1C\uC778",
            [
                new("\uC0BC\uC131\uC804\uC790", "1,000", "SK\uD558\uC774\uB2C9\uC2A4", "5,000"),
                null,
                new("A", "", "", ""),
                null
            ]);

        var actual = new S5025SceneMutationBuilder().Build(data).Select(Describe);

        Assert.Equal(
        [
            "V:title=\uAC1C\uC778 \uC21C\uB9E4\uB3C4 \uC0C1\uC704",
            "V:\uC885\uBAA91=\uC0BC\uC131\uC804\uC790", "V:\uAE08\uC5611=1,000",
            "V:\uC885\uBAA92=SK\uD558\uC774\uB2C9\uC2A4", "V:\uAE08\uC5612=5,000",
            "V:\uC885\uBAA93=", "V:\uAE08\uC5613=", "V:\uC885\uBAA94=", "V:\uAE08\uC5614=",
            "V:\uC885\uBAA95=A", "V:\uAE08\uC5615=", "V:\uC885\uBAA96=", "V:\uAE08\uC5616=",
            "V:\uC885\uBAA97=", "V:\uAE08\uC5617=", "V:\uC885\uBAA98=", "V:\uAE08\uC5618="
        ], actual);
    }

    [Fact]
    public void S5037_matches_flat_quote_clear_then_buy_sell_fill_order()
    {
        var data = new S5037SceneData(
            "A",
            10_000,
            0,
            0,
            ScenePriceDirection.Flat,
            [new("BuyA", 123), new("BuyZero", 0)],
            [new("SellNegative", -10)]);

        var actual = new S5037SceneMutationBuilder().Build(data).Select(Describe);

        Assert.Equal(
        [
            "V:title=A", "V:price=10,000",
            "V:changePrice=0", "V:rate=0.00",
            "S:upup=0", "S:up=0", "S:flat=0", "S:down=0", "S:downdown=0",
            "S:bg1=0", "S:bg2=1", "S:bg3=0",
            "V:buy_title_1=", "V:buy_vol_1=", "V:sell_title_1=", "V:sell_vol_1=",
            "V:buy_title_2=", "V:buy_vol_2=", "V:sell_title_2=", "V:sell_vol_2=",
            "V:buy_title_3=", "V:buy_vol_3=", "V:sell_title_3=", "V:sell_vol_3=",
            "V:buy_title_4=", "V:buy_vol_4=", "V:sell_title_4=", "V:sell_vol_4=",
            "V:buy_title_5=", "V:buy_vol_5=", "V:sell_title_5=", "V:sell_vol_5=",
            "V:buy_title_1=BuyA", "V:buy_vol_1=123",
            "V:buy_title_2=BuyZero", "V:buy_vol_2=0",
            "V:sell_title_1=SellNegative", "V:sell_vol_1=-10"
        ], actual);
    }

    [Theory]
    [InlineData(ScenePriceDirection.Up, "S:up=1", "S:bg1=1")]
    [InlineData(ScenePriceDirection.Down, "S:down=1", "S:bg3=1")]
    [InlineData(ScenePriceDirection.Flat, null, "S:bg2=1")]
    public void S5037_direction_variants_keep_original_visibility_targets(
        ScenePriceDirection direction,
        string? activeDirection,
        string activeBackground)
    {
        var actual = new S5037SceneMutationBuilder().Build(new S5037SceneData(
            "A", 1, 0, 0, direction, [], [])).Select(Describe).ToArray();

        Assert.Equal(
            ["S:upup=0", "S:up=0", "S:flat=0", "S:down=0", "S:downdown=0"],
            actual.Skip(4).Take(5));
        if (activeDirection is null)
        {
            Assert.DoesNotContain(
                actual,
                value => value is "S:upup=1" or "S:up=1" or "S:flat=1" or
                    "S:down=1" or "S:downdown=1");
        }
        else
        {
            Assert.Contains(activeDirection, actual);
        }

        Assert.Contains(activeBackground, actual);
    }

    [Fact]
    public void S8003_matches_quote_price_color_quantity_and_scale_order_with_empty_levels()
    {
        var data = new S8003SceneData(
            "A",
            100,
            5,
            1.25,
            ScenePriceDirection.Up,
            [new(110, 100), new(90, 0), null],
            [new(100, 50), new(120, 25), null],
            100,
            75);

        var actual = new S8003SceneMutationBuilder().Build(data).Select(Describe);

        Assert.Equal(
        [
            "V:title=A", "V:price=100",
            "V:changePrice=5", "V:rate=1.25",
            "S:upup=0", "S:up=0", "S:flat=0", "S:down=0", "S:downdown=0",
            "S:up=1", "S:bg1=1", "S:bg2=0", "S:bg3=0",
            "V:price_buy_1=110", "C:price_buy_1:172:0:0:255",
            "V:price_buy_2=90", "C:price_buy_2:19:73:170:255",
            "V:price_buy_3=", "C:price_buy_3:50:51:50:255",
            "V:buy_jan_1=100", "V:buy_jan_2=0", "V:buy_jan_3=",
            "V:price_sell_1=100", "C:price_sell_1:50:51:50:255",
            "V:price_sell_2=120", "C:price_sell_2:172:0:0:255",
            "V:price_sell_3=", "C:price_sell_3:50:51:50:255",
            "V:sell_jan_1=50", "V:sell_jan_2=25", "V:sell_jan_3=",
            "V:buy_jan_total=100", "V:sell_jan_total=75",
            "Z:buy_bar_1:1:0:0:1", "Z:buy_bar_2:0:0:0:1", "Z:buy_bar_3:0:0:0:1",
            "Z:sell_bar_1:0.5:0:0:1", "Z:sell_bar_2:0.25:0:0:1", "Z:sell_bar_3:0:0:0:1"
        ], actual);
    }

    [Fact]
    public void S8003_all_empty_levels_emit_finite_zero_scales()
    {
        var actual = new S8003SceneMutationBuilder().Build(new S8003SceneData(
            "A",
            0,
            0,
            0,
            ScenePriceDirection.Unknown,
            [null, null, null],
            [null, null, null],
            0,
            0));

        Assert.DoesNotContain(actual.Select(Describe), value =>
            value.Contains("NaN", StringComparison.Ordinal));
        Assert.All(actual.TakeLast(6), mutation =>
            Assert.Contains(":0:0:0:1", Describe(mutation), StringComparison.Ordinal));
    }

    [Fact]
    public void Builders_reject_unclosed_or_malformed_data_before_dynamic_names_are_possible()
    {
        Assert.Throws<LegacySceneDataException>(() =>
            new S5023SceneMutationBuilder().Build(new S5023SceneData(
                (LegacyDomesticMarket)99,
                [null, null, null, null, null],
                new(0, 0, 0))));
        Assert.Throws<LegacySceneDataException>(() =>
            new S5024SceneMutationBuilder().Build(new S5024SceneData(
                LegacyDomesticMarket.Kospi,
                [new((InvestorParticipant)99, 1)])));
        Assert.Throws<LegacySceneDataException>(() =>
            new S5025SceneMutationBuilder().Build(new S5025SceneData("A", [])));
        Assert.Throws<LegacySceneDataException>(() =>
            new S8003SceneMutationBuilder().Build(new S8003SceneData(
                "A", 1, 0, 0, ScenePriceDirection.Flat,
                [new(1, -1), null, null],
                [null, null, null],
                0,
                0)));

        var dtoTypes = new[]
        {
            typeof(S5023SceneData), typeof(S5024SceneData), typeof(S5025SceneData),
            typeof(S5037SceneData), typeof(S8003SceneData)
        };
        Assert.All(dtoTypes, dtoType =>
        {
            Assert.True(typeof(ILegacySceneData).IsAssignableFrom(dtoType));
            Assert.DoesNotContain(
                dtoType.GetProperties(BindingFlags.Instance | BindingFlags.Public),
                property => property.Name is "ObjectName" or "MethodName" or "AssetPath" or "SceneFile");
        });
    }

    private static string Describe(PlayoutMutation mutation) => mutation switch
    {
        PlayoutSetValue value => $"V:{value.ObjectName}={value.Value}",
        PlayoutSetVisible visible => $"S:{visible.ObjectName}={(visible.IsVisible ? 1 : 0)}",
        PlayoutSetFaceColor color =>
            $"C:{color.ObjectName}:{color.Red}:{color.Green}:{color.Blue}:{color.Alpha}",
        PlayoutSetPosition position => FormattableString.Invariant(
            $"P:{position.ObjectName}:{position.X}:{position.Y}:{position.Z}:{(int)position.Components}"),
        PlayoutSetScale scale => FormattableString.Invariant(
            $"Z:{scale.ObjectName}:{scale.X}:{scale.Y}:{scale.Z}:{(int)scale.Components}"),
        _ => mutation.GetType().Name
    };
}
