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

namespace MBN_STOCK_WEBVIEW.Core.Tests;

public sealed class ChartSceneBuilders5078To5084Tests
{
    [Fact]
    public void S5078_matches_complete_legacy_visibility_value_and_scale_order()
    {
        var data = new S5078SceneData(
            SectorIndexMarket.Kospi,
            SectorRows(
                ("A", 100, SectorBarVariant.Secondary),
                ("B", -50, SectorBarVariant.Primary),
                ("C", 0, SectorBarVariant.Secondary),
                ("D", 25, SectorBarVariant.Secondary),
                ("E", -25, SectorBarVariant.Primary),
                ("F", 10, SectorBarVariant.Secondary),
                ("G", -10, SectorBarVariant.Primary),
                ("H", 5, SectorBarVariant.Secondary),
                ("I", -5, SectorBarVariant.Primary),
                ("J", 50, SectorBarVariant.Secondary)));

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

        Assert.Equal(
        [
            "V:title=코스피 섹터지수",
            "S:bar1_1=0", "S:value1_1=0", "S:bar1_2=0", "S:value1_2=0",
            "S:bar2_1=0", "S:value2_1=0", "S:bar2_2=0", "S:value2_2=0",
            "S:bar3_1=0", "S:value3_1=0", "S:bar3_2=0", "S:value3_2=0",
            "S:bar4_1=0", "S:value4_1=0", "S:bar4_2=0", "S:value4_2=0",
            "S:bar5_1=0", "S:value5_1=0", "S:bar5_2=0", "S:value5_2=0",
            "S:bar6_1=0", "S:value6_1=0", "S:bar6_2=0", "S:value6_2=0",
            "S:bar7_1=0", "S:value7_1=0", "S:bar7_2=0", "S:value7_2=0",
            "S:bar8_1=0", "S:value8_1=0", "S:bar8_2=0", "S:value8_2=0",
            "S:bar9_1=0", "S:value9_1=0", "S:bar9_2=0", "S:value9_2=0",
            "S:bar10_1=0", "S:value10_1=0", "S:bar10_2=0", "S:value10_2=0",
            "V:category1=A", "S:bar1_2=1", "S:value1_2=1",
            "Z:bar1_2:1:0:0:1", "V:value1_2=100.00",
            "V:category2=B", "S:bar2_1=1", "S:value2_1=1",
            "Z:bar2_1:-0.5:0:0:1", "V:value2_1=-50.00",
            "V:category3=C", "S:bar3_2=1", "S:value3_2=1",
            "Z:bar3_2:0:0:0:1", "V:value3_2=0.00",
            "V:category4=D", "S:bar4_2=1", "S:value4_2=1",
            "Z:bar4_2:0.25:0:0:1", "V:value4_2=25.00",
            "V:category5=E", "S:bar5_1=1", "S:value5_1=1",
            "Z:bar5_1:-0.25:0:0:1", "V:value5_1=-25.00",
            "V:category6=F", "S:bar6_2=1", "S:value6_2=1",
            "Z:bar6_2:0.1:0:0:1", "V:value6_2=10.00",
            "V:category7=G", "S:bar7_1=1", "S:value7_1=1",
            "Z:bar7_1:-0.1:0:0:1", "V:value7_1=-10.00",
            "V:category8=H", "S:bar8_2=1", "S:value8_2=1",
            "Z:bar8_2:0.05:0:0:1", "V:value8_2=5.00",
            "V:category9=I", "S:bar9_1=1", "S:value9_1=1",
            "Z:bar9_1:-0.05:0:0:1", "V:value9_1=-5.00",
            "V:category10=J", "S:bar10_2=1", "S:value10_2=1",
            "Z:bar10_2:0.5:0:0:1", "V:value10_2=50.00"
        ], actual);
    }

    [Fact]
    public void S5078_zero_and_us_primary_values_are_finite_and_use_original_sign_factor()
    {
        var zeroRows = Enumerable.Range(1, 10)
            .Select(index => new SectorIndexBarData(
                "Z" + index,
                0,
                SectorBarVariant.Primary))
            .ToArray();
        var zeros = new S5078SceneMutationBuilder().Build(new S5078SceneData(
            SectorIndexMarket.Kosdaq,
            zeroRows));

        AssertFinite(zeros);
        Assert.All(zeros.OfType<PlayoutSetScale>(), scale => Assert.Equal(0, scale.X));

        var usRows = zeroRows.ToArray();
        usRows[0] = new SectorIndexBarData("MAX", 100, SectorBarVariant.Secondary);
        usRows[1] = new SectorIndexBarData("NEG", -50, SectorBarVariant.Primary);
        var us = new S5078SceneMutationBuilder().Build(new S5078SceneData(
            SectorIndexMarket.Dow,
            usRows));

        Assert.Contains("Z:bar2_1:0.5:0:0:1", us.Select(Describe));
    }

    [Fact]
    public void S5078_rejects_empty_nonfinite_and_unmapped_data()
    {
        var builder = new S5078SceneMutationBuilder();
        Assert.Throws<LegacySceneDataException>(() => builder.Build(new S5078SceneData(
            SectorIndexMarket.Kospi,
            [])));
        Assert.Throws<LegacySceneDataException>(() => builder.Build(new S5078SceneData(
            SectorIndexMarket.Kospi,
            SectorRows(("bad", double.PositiveInfinity, SectorBarVariant.Primary)))));
        Assert.Throws<LegacySceneDataException>(() => builder.Build(new S5078SceneData(
            (SectorIndexMarket)99,
            SectorRows())));
        Assert.Throws<LegacySceneDataException>(() => builder.Build(new S5078SceneData(
            SectorIndexMarket.Kospi,
            SectorRows(("bad", 1, (SectorBarVariant)99)))));
    }

    [Fact]
    public void S5079_matches_complete_legacy_axis_visibility_path_and_date_order()
    {
        var data = new S5079SceneData(
            "A사",
            [
                new(GrowthMetricPath.First, [0d, 25d, 50d, 100d]),
                new(GrowthMetricPath.Second, [-100d, -50d, 0d, 50d]),
                new(GrowthMetricPath.Third, [null, 0d, 25d, 75d]),
                new(GrowthMetricPath.Fourth, [100d, 100d, 100d, 100d])
            ],
            ["2025Q1", "2025Q2", "2025Q3", "2025Q4"]);

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

        Assert.Equal(
        [
            "V:title=A사 성장성 지표",
            "V:value1=-60", "V:value2=-20", "V:value3=20", "V:value4=60", "V:value5=100",
            "S:path1=0", "S:path1=1", "S:path1=1", "S:path1=1", "S:path1=1",
            "L:path1=0,0,0|368,36,0|740,72,0|1110,144,0",
            "S:path2=0", "S:path2=1", "S:path2=1", "S:path2=1", "S:path2=1",
            "L:path2=0,-144,0|368,-72,0|740,0,0|1110,72,0",
            "L:path3=0,0,0|368,0,0|740,36,0|1110,108,0", "S:path3=0",
            "S:path4=0", "S:path4=1", "S:path4=1", "S:path4=1", "S:path4=1",
            "L:path4=0,144,0|368,144,0|740,144,0|1110,144,0",
            "V:date1=2025Q1", "V:date2=2025Q2", "V:date3=2025Q3", "V:date4=2025Q4"
        ], actual);
    }

    [Fact]
    public void S5079_equal_and_empty_tokens_normalize_to_finite_center_points()
    {
        var equal = new S5079SceneMutationBuilder().Build(new S5079SceneData(
            "A사",
            Enum.GetValues<GrowthMetricPath>()
                .Select(path => new GrowthMetricSeriesData(path, [5d, 5d, 5d, 5d]))
                .ToArray(),
            ["", "", "", ""]));
        AssertFinite(equal);
        Assert.All(
            equal.OfType<PlayoutSetPathPoints>().SelectMany(path => path.Points),
            point => Assert.Equal(0, point.Y));

        var emptyTokens = new S5079SceneMutationBuilder().Build(new S5079SceneData(
            "A사",
            Enum.GetValues<GrowthMetricPath>()
                .Select(path => new GrowthMetricSeriesData(path, [null, null, null, null]))
                .ToArray(),
            ["", "", "", ""]));
        AssertFinite(emptyTokens);
        Assert.All(
            emptyTokens.OfType<PlayoutSetPathPoints>().SelectMany(path => path.Points),
            point => Assert.Equal(0, point.Y));
    }

    [Fact]
    public void S5079_rejects_empty_duplicate_nonfinite_and_unmapped_series()
    {
        var builder = new S5079SceneMutationBuilder();
        Assert.Throws<LegacySceneDataException>(() => builder.Build(new S5079SceneData(
            "A사", [], ["1", "2", "3", "4"])));
        Assert.Throws<LegacySceneDataException>(() => builder.Build(new S5079SceneData(
            "A사",
            [
                new(GrowthMetricPath.First, [1d, 2d, 3d, 4d]),
                new(GrowthMetricPath.First, [1d, 2d, 3d, 4d]),
                new(GrowthMetricPath.Third, [1d, 2d, 3d, 4d]),
                new(GrowthMetricPath.Fourth, [1d, 2d, 3d, 4d])
            ],
            ["1", "2", "3", "4"])));
        Assert.Throws<LegacySceneDataException>(() => builder.Build(new S5079SceneData(
            "A사",
            [
                new(GrowthMetricPath.First, [double.NaN, 2d, 3d, 4d]),
                new(GrowthMetricPath.Second, [1d, 2d, 3d, 4d]),
                new(GrowthMetricPath.Third, [1d, 2d, 3d, 4d]),
                new(GrowthMetricPath.Fourth, [1d, 2d, 3d, 4d])
            ],
            ["1", "2", "3", "4"])));
        Assert.Throws<LegacySceneDataException>(() => builder.Build(new S5079SceneData(
            "A사",
            [
                new((GrowthMetricPath)99, [1d, 2d, 3d, 4d]),
                new(GrowthMetricPath.Second, [1d, 2d, 3d, 4d]),
                new(GrowthMetricPath.Third, [1d, 2d, 3d, 4d]),
                new(GrowthMetricPath.Fourth, [1d, 2d, 3d, 4d])
            ],
            ["1", "2", "3", "4"])));
    }

    [Fact]
    public void S5080_matches_complete_legacy_quarter_group_center_and_scale_order()
    {
        var data = new S5080SceneData(
            "A사",
            [
                new("1Q", 100), new("2Q", -50), new("3Q", 25),
                new("4Q", 0), new("5Q", 50), new("6Q", -25)
            ]);

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

        Assert.Equal(
        [
            "V:title=A사 매출액",
            "V:quarter1=1Q", "V:quarter2=2Q", "V:quarter3=3Q",
            "V:quarter4=4Q", "V:quarter5=5Q", "V:quarter6=6Q",
            "S:barG1_1=0", "S:barG1_2=0", "S:barG1_1=1", "V:value1_1=100",
            "S:barG2_1=0", "S:barG2_2=0", "S:barG2_2=1", "V:value2_2=-50",
            "S:barG3_1=0", "S:barG3_2=0", "S:barG3_1=1", "V:value3_1=25",
            "S:barG4_1=0", "S:barG4_2=0", "S:barG4_1=1", "V:value4_1=0",
            "S:barG5_1=0", "S:barG5_2=0", "S:barG5_1=1", "V:value5_1=50",
            "S:barG6_1=0", "S:barG6_2=0", "S:barG6_2=1", "V:value6_2=-25",
            "K:centerbar:0:0:-131.66667:0:2",
            "Z:bar1_1:0:1.2:0:2", "Z:bar2_2:0:0.6:0:2",
            "Z:bar3_1:0:0.3:0:2", "Z:bar4_1:0:0:0:2",
            "Z:bar5_1:0:0.6:0:2", "Z:bar6_2:0:0.3:0:2"
        ], actual);
    }

    [Fact]
    public void S5080_zero_positive_negative_and_equal_values_stay_finite()
    {
        var cases = new[]
        {
            (Value: 0, Center: -250f, Scale: 0f),
            (Value: 10, Center: -240f, Scale: 1.8f),
            (Value: -10, Center: 115f, Scale: 1.8f)
        };
        foreach (var item in cases)
        {
            var mutations = new S5080SceneMutationBuilder().Build(new S5080SceneData(
                "A사",
                Enumerable.Range(1, 6)
                    .Select(index => new QuarterlySalesData(index + "Q", item.Value))
                    .ToArray()));

            AssertFinite(mutations);
            Assert.Equal(item.Center, Assert.Single(mutations.OfType<PlayoutSetPositionKey>()).Y);
            Assert.All(mutations.OfType<PlayoutSetScale>(), scale => Assert.Equal(item.Scale, scale.Y));
        }

        Assert.Throws<LegacySceneDataException>(() =>
            new S5080SceneMutationBuilder().Build(new S5080SceneData("A사", [])));
    }

    [Fact]
    public void S5083_matches_complete_legacy_summary_baseline_and_path_order()
    {
        var data = new S5083SceneData(
            InvestorFlowParticipant.Individual,
            [
                new(InvestorFlowMarket.Kospi, 1000),
                new(InvestorFlowMarket.Kosdaq, -500),
                new(InvestorFlowMarket.Kospi200, 0)
            ],
            [
                new(InvestorFlowMarket.Kospi, -100, new TimeOnly(9, 0)),
                new(InvestorFlowMarket.Kosdaq, 0, new TimeOnly(9, 0)),
                new(InvestorFlowMarket.Kospi200, 100, new TimeOnly(9, 0)),
                new(InvestorFlowMarket.Kospi, 100, new TimeOnly(15, 0)),
                new(InvestorFlowMarket.Kosdaq, -50, new TimeOnly(15, 10)),
                new(InvestorFlowMarket.Kospi200, 50, new TimeOnly(15, 30))
            ]);

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

        Assert.Equal(
        [
            "V:title=개인 매매동향",
            "V:price1=1,000", "V:price2=-500", "V:price3=0",
            "P:baseline:0:43:0:2",
            "L:path1=0,-5,0|0,-138,0|1212.26,128,0",
            "L:path2=0,-5,0|606.13,-5,0|1212.26,-71.5,0",
            "L:path3=0,-5,0|606.13,128,0|1212.26,61.5,0"
        ], actual);
    }

    [Fact]
    public void S5083_zero_single_points_and_preopen_time_use_explicit_finite_normalization()
    {
        var data = new S5083SceneData(
            InvestorFlowParticipant.Foreign,
            [
                new(InvestorFlowMarket.Kospi, 0),
                new(InvestorFlowMarket.Kosdaq, 0),
                new(InvestorFlowMarket.Kospi200, 0)
            ],
            [
                new(InvestorFlowMarket.Kospi, 0, new TimeOnly(8, 0)),
                new(InvestorFlowMarket.Kosdaq, 0, new TimeOnly(8, 0)),
                new(InvestorFlowMarket.Kospi200, 0, new TimeOnly(8, 0))
            ]);

        var mutations = new S5083SceneMutationBuilder().Build(data);

        AssertFinite(mutations);
        Assert.Equal(43, Assert.Single(mutations.OfType<PlayoutSetPosition>()).Y);
        Assert.All(
            mutations.OfType<PlayoutSetPathPoints>().SelectMany(path => path.Points),
            point =>
            {
                Assert.Equal(0, point.X);
                Assert.Equal(-5, point.Y);
            });
    }

    [Fact]
    public void S5083_rejects_empty_missing_duplicate_and_unmapped_targets()
    {
        var summary = new InvestorFlowSummaryData[]
        {
            new(InvestorFlowMarket.Kospi, 0),
            new(InvestorFlowMarket.Kosdaq, 0),
            new(InvestorFlowMarket.Kospi200, 0)
        };
        var points = new InvestorFlowTrendPoint[]
        {
            new(InvestorFlowMarket.Kospi, 0, new TimeOnly(9, 0)),
            new(InvestorFlowMarket.Kosdaq, 0, new TimeOnly(9, 0)),
            new(InvestorFlowMarket.Kospi200, 0, new TimeOnly(9, 0))
        };
        var builder = new S5083SceneMutationBuilder();

        Assert.Throws<LegacySceneDataException>(() => builder.Build(new S5083SceneData(
            InvestorFlowParticipant.Individual, summary, [])));
        Assert.Throws<LegacySceneDataException>(() => builder.Build(new S5083SceneData(
            InvestorFlowParticipant.Individual,
            [summary[0], summary[0], summary[2]],
            points)));
        Assert.Throws<LegacySceneDataException>(() => builder.Build(new S5083SceneData(
            InvestorFlowParticipant.Individual,
            summary,
            [points[0], points[0], points[2]])));
        Assert.Throws<LegacySceneDataException>(() => builder.Build(new S5083SceneData(
            (InvestorFlowParticipant)99,
            summary,
            points)));
        Assert.Throws<LegacySceneDataException>(() => builder.Build(new S5083SceneData(
            InvestorFlowParticipant.Individual,
            summary,
            [points[0], points[1], new((InvestorFlowMarket)99, 0, new TimeOnly(9, 0))])));
    }

    [Fact]
    public void S5084_matches_complete_legacy_totals_baseline_and_path_order()
    {
        var data = new S5084SceneData(
            InvestorFlowIndex.Kospi,
            new InvestorFlowTotalsData(1000, -500, 250),
            [
                new(-100, 0, 100, new TimeOnly(9, 0)),
                new(100, -50, 50, new TimeOnly(15, 30))
            ]);

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

        Assert.Equal(
        [
            "V:title=코스피 매매동향",
            "V:price1=1,000", "V:price2=-500", "V:price3=250",
            "P:baseline:0:43:0:2",
            "L:path1=0,-5,0|606.13,-138,0|1212.26,128,0",
            "L:path2=0,-5,0|606.13,-5,0|1212.26,-71.5,0",
            "L:path3=0,-5,0|606.13,128,0|1212.26,61.5,0"
        ], actual);
    }

    [Fact]
    public void S5084_zero_single_row_is_finite_and_empty_or_unmapped_data_fails_closed()
    {
        var builder = new S5084SceneMutationBuilder();
        var zero = builder.Build(new S5084SceneData(
            InvestorFlowIndex.Kosdaq,
            new InvestorFlowTotalsData(0, 0, 0),
            [new(0, 0, 0, new TimeOnly(15, 30))]));

        AssertFinite(zero);
        Assert.Equal(43, Assert.Single(zero.OfType<PlayoutSetPosition>()).Y);
        Assert.All(
            zero.OfType<PlayoutSetPathPoints>(),
            path =>
            {
                Assert.Equal(-5, path.Points[0].Y);
                Assert.Equal(1212.26f, path.Points[1].X);
                Assert.Equal(-5, path.Points[1].Y);
            });

        Assert.Throws<LegacySceneDataException>(() => builder.Build(new S5084SceneData(
            InvestorFlowIndex.Kospi,
            new InvestorFlowTotalsData(0, 0, 0),
            [])));
        Assert.Throws<LegacySceneDataException>(() => builder.Build(new S5084SceneData(
            (InvestorFlowIndex)99,
            new InvestorFlowTotalsData(0, 0, 0),
            [new(0, 0, 0, new TimeOnly(15, 30))])));
    }

    [Fact]
    public void Builders_expose_the_closed_scene_keys()
    {
        Assert.Equal("s5078", new S5078SceneMutationBuilder().BuilderKey);
        Assert.Equal("s5079", new S5079SceneMutationBuilder().BuilderKey);
        Assert.Equal("s5080", new S5080SceneMutationBuilder().BuilderKey);
        Assert.Equal("s5083", new S5083SceneMutationBuilder().BuilderKey);
        Assert.Equal("s5084", new S5084SceneMutationBuilder().BuilderKey);
    }

    private static SectorIndexBarData[] SectorRows(
        params (string Category, double Value, SectorBarVariant Variant)[] overrides)
    {
        var rows = Enumerable.Range(1, 10)
            .Select(index => new SectorIndexBarData(
                "R" + index,
                0,
                SectorBarVariant.Secondary))
            .ToArray();
        for (var index = 0; index < overrides.Length; index++)
        {
            rows[index] = new SectorIndexBarData(
                overrides[index].Category,
                overrides[index].Value,
                overrides[index].Variant);
        }

        return rows;
    }

    private static void AssertFinite(IEnumerable<PlayoutMutation> mutations)
    {
        foreach (var mutation in mutations)
        {
            switch (mutation)
            {
                case PlayoutSetPosition position:
                    Assert.True(float.IsFinite(position.X));
                    Assert.True(float.IsFinite(position.Y));
                    Assert.True(float.IsFinite(position.Z));
                    break;
                case PlayoutSetPositionKey positionKey:
                    Assert.True(float.IsFinite(positionKey.X));
                    Assert.True(float.IsFinite(positionKey.Y));
                    Assert.True(float.IsFinite(positionKey.Z));
                    break;
                case PlayoutSetScale scale:
                    Assert.True(float.IsFinite(scale.X));
                    Assert.True(float.IsFinite(scale.Y));
                    Assert.True(float.IsFinite(scale.Z));
                    break;
                case PlayoutSetPathPoints path:
                    Assert.All(path.Points, point =>
                    {
                        Assert.True(float.IsFinite(point.X));
                        Assert.True(float.IsFinite(point.Y));
                        Assert.True(float.IsFinite(point.Z));
                    });
                    break;
                case PlayoutSetValue value:
                    Assert.DoesNotContain("NaN", value.Value, StringComparison.OrdinalIgnoreCase);
                    Assert.DoesNotContain("Infinity", value.Value, StringComparison.OrdinalIgnoreCase);
                    break;
            }
        }
    }

    private static string Describe(PlayoutMutation mutation) => mutation switch
    {
        PlayoutSetValue value => $"V:{value.ObjectName}={value.Value}",
        PlayoutSetVisible visible => $"S:{visible.ObjectName}={(visible.IsVisible ? 1 : 0)}",
        PlayoutSetPosition position => FormattableString.Invariant(
            $"P:{position.ObjectName}:{position.X}:{position.Y}:{position.Z}:{(int)position.Components}"),
        PlayoutSetPositionKey key => FormattableString.Invariant(
            $"K:{key.ObjectName}:{key.KeyIndex}:{key.X}:{key.Y}:{key.Z}:{(int)key.Components}"),
        PlayoutSetScale scale => FormattableString.Invariant(
            $"Z:{scale.ObjectName}:{scale.X}:{scale.Y}:{scale.Z}:{(int)scale.Components}"),
        PlayoutSetPathPoints path => "L:" + path.ObjectName + "=" + string.Join(
            "|",
            path.Points.Select(point => FormattableString.Invariant(
                $"{point.X},{point.Y},{point.Z}"))),
        _ => mutation.GetType().Name
    };
}
