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 CandleSceneBuilderTests
{
    public static TheoryData<CandleCutAlias, CandlePeriod, float, string> AliasCases => new()
    {
        { CandleCutAlias.Cut8035, CandlePeriod.Intraday, 0f, "50ECC21B9F7DEB27EA154C61B080899D1D4FF2C1D490F38B8285D9E46B881D6F" },
        { CandleCutAlias.Cut8061, CandlePeriod.FiveDays, 1188f, "CC3D1389889FD7F79850A09F7C470723EE5D3C81A71D3CDC8C506B560C23269C" },
        { CandleCutAlias.Cut8040, CandlePeriod.TwentyDays, 1398f, "FD0B72C1748964FDEB4188CBC9A225951E6020B7D0F169CCA303A70044CDD499" },
        { CandleCutAlias.Cut8046, CandlePeriod.SixtyDays, 1442f, "CA04EC110D1E347D4824DB076781AD1D515A46B2E1FCDD6488196A044661012F" },
        { CandleCutAlias.Cut8051, CandlePeriod.OneHundredTwentyDays, 1452f, "0E3442C5393AE644FA471DAB68696FFBDF1CBD7D420A0155DC8CFCC3BE1202CC" },
        { CandleCutAlias.Cut8056, CandlePeriod.TwoHundredFortyDays, 1464f, "D6ABC34111DAC4B32F8D1CA5563EAA248BA5B1C0C782B74E53A9C651BBC98D48" }
    };

    public static TheoryData<MarketCase> MarketCases => new()
    {
        new(
            CandleMarketTarget.KospiIndex,
            CandleChartMode.Price,
            "ignored",
            "\uCF54\uC2A4\uD53C \uC9C0\uC218",
            "\uB2E8\uC704 : p",
            "100.00"),
        new(
            CandleMarketTarget.KosdaqIndex,
            CandleChartMode.Price,
            "ignored",
            "\uCF54\uC2A4\uB2E5 \uC9C0\uC218",
            "\uB2E8\uC704 : p",
            "100.00"),
        new(
            CandleMarketTarget.Kospi200Index,
            CandleChartMode.Price,
            "ignored",
            "\uCF54\uC2A4\uD53C200 \uC9C0\uC218",
            "\uB2E8\uC704 : p",
            "100.00"),
        new(
            CandleMarketTarget.Krx100Index,
            CandleChartMode.Price,
            "ignored",
            "KRX100 \uC9C0\uC218",
            "\uB2E8\uC704 : p",
            "100.00"),
        new(
            CandleMarketTarget.FuturesIndex,
            CandleChartMode.ExpectedExecution,
            "ignored",
            "\uC120\uBB3C \uC608\uC0C1\uCCB4\uACB0\uC9C0\uC218",
            "\uB2E8\uC704 : p",
            "100.00"),
        new(
            CandleMarketTarget.KospiIndustry,
            CandleChartMode.Price,
            "Banks",
            "\uCF54\uC2A4\uD53C - Banks",
            "\uB2E8\uC704 : p",
            "100.00"),
        new(
            CandleMarketTarget.KosdaqIndustry,
            CandleChartMode.ExpectedExecution,
            "Tech",
            "Tech \uC608\uC0C1\uCCB4\uACB0",
            null,
            "100.00"),
        new(
            CandleMarketTarget.OverseasIndex,
            CandleChartMode.Price,
            "Dow",
            "Dow",
            "\uB2E8\uC704 : p",
            "100.00"),
        new(
            CandleMarketTarget.OverseasStock,
            CandleChartMode.Price,
            "NVDA",
            "NVDA",
            "\uB2E8\uC704 : \uB2EC\uB7EC",
            "100.00"),
        new(
            CandleMarketTarget.KospiStock,
            CandleChartMode.ExpectedExecution,
            "Samsung",
            "Samsung \uC608\uC0C1\uCCB4\uACB0",
            null,
            "100"),
        new(
            CandleMarketTarget.KosdaqStock,
            CandleChartMode.TradingHalt,
            "Halt",
            "Halt",
            "\uB2E8\uC704 : \uC6D0",
            "100")
    };

    [Fact]
    public void Volume_candle_matches_legacy_setup_quote_crops_colors_paths_and_partial_points()
    {
        var data = new S8010SceneData(
            CandleCutAlias.Cut8061,
            CandleMarketTarget.KospiStock,
            CandleChartMode.PriceWithVolume,
            "Stock",
            new CandleQuoteData(110, -10, -8.33, ScenePriceDirection.Down),
            [
                Row(1, 100, 120, 90, 110, 100, 105, null),
                Row(2, 110, 125, 80, 100, 0, null, 100),
                Row(3, 100, 100, 100, 100, 50, 100, 100)
            ],
            ShowMovingAverage5: true,
            ShowMovingAverage20: true);

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

        Assert.Equal(
        [
            "V:title=Stock", "L:ma5=", "L:ma20=",
            "V:unit=\uB2E8\uC704 : \uC6D0", "V:title=Stock",
            "V:price=110", "V:changePrice=-10", "V:rate=-8.33",
            "S:upup=0", "S:up=0", "S:flat=0", "S:down=0", "S:downdown=0",
            "S:down=1", "S:bg1=0", "S:bg2=0", "S:bg3=1",
            "S:volrumeG=1"
        ], actual.Take(18));
        Assert.Equal("S:volume1=0", actual[18]);
        Assert.Equal("S:volume5=0", actual[22]);
        Assert.Equal("S:candle1=0", actual[23]);
        Assert.Equal("S:candle5=0", actual[27]);
        Assert.Equal("V:date_begin=07/01", actual[28]);
        Assert.Contains("C:candlebody1:170:0:0:255", actual);
        Assert.Contains("C:candlebar2:15:99:189:255", actual);
        Assert.Contains("C:candlebody3:95:95:95:255", actual);
        Assert.Contains("K:volume2:1:0:100:0:100:10", actual);

        var ma5 = Assert.Single(
            mutations.OfType<PlayoutSetPathPoints>(),
            path => path.ObjectName == "ma5" && path.Points.Count != 0);
        Assert.Single(ma5.Points);
        Assert.Empty(mutations.OfType<PlayoutSetPathPoints>()
            .Last(path => path.ObjectName == "ma20").Points);
        Assert.Equal(
            "3493F848239713D995223BD6306CFF54AD0568093314FC1ADA688455919F7527",
            SnapshotHash(actual));
    }

    [Theory]
    [MemberData(nameof(AliasCases))]
    public void Every_alias_maps_to_its_period_width_dates_and_full_golden_sequence(
        CandleCutAlias alias,
        CandlePeriod expectedPeriod,
        float expectedSecondX,
        string expectedHash)
    {
        var rows = new[]
        {
            Row(1, 100, 120, 90, 110, 10, 105, 104, new TimeOnly(9, 0)),
            Row(2, 110, 125, 95, 100, 20, 106, 105, new TimeOnly(9, 5))
        };
        var data = new S8010SceneData(
            alias,
            CandleMarketTarget.KospiStock,
            CandleChartMode.Price,
            "A",
            new CandleQuoteData(100, 1, 1, ScenePriceDirection.Up),
            rows,
            ShowMovingAverage5: true,
            ShowMovingAverage20: false);

        var mutations = new S8010SceneMutationBuilder().Build(data);
        Assert.Equal(expectedPeriod, CandleCutAliasMetadata.GetPeriod(alias));
        if (CandleCutAliasMetadata.SupportsMovingAveragePaths(alias))
        {
            var ma5 = mutations.OfType<PlayoutSetPathPoints>()
                .Last(path => path.ObjectName == "ma5");
            Assert.Equal(2, ma5.Points.Count);
            Assert.Equal(0f, ma5.Points[0].X);
            Assert.Equal(expectedSecondX, ma5.Points[1].X);
        }
        else
        {
            Assert.DoesNotContain(mutations, mutation => mutation is PlayoutSetPathPoints);
        }
        if (expectedPeriod == CandlePeriod.Intraday)
        {
            Assert.Contains(mutations, mutation =>
                mutation is PlayoutSetValue { ObjectName: "date_begin", Value: "09:00" });
            Assert.Contains(mutations, mutation =>
                mutation is PlayoutSetValue { ObjectName: "date_end", Value: "15:30" });
        }
        else
        {
            Assert.Contains(mutations, mutation =>
                mutation is PlayoutSetValue { ObjectName: "date_begin", Value: "07/01" });
            Assert.Contains(mutations, mutation =>
                mutation is PlayoutSetValue { ObjectName: "date_end", Value: "07/02" });
        }

        var hash = SnapshotHash(mutations.Select(Describe));
        Assert.Equal(expectedHash, hash);
    }

    [Theory]
    [MemberData(nameof(MarketCases))]
    public void Market_and_chart_targets_use_closed_title_unit_and_precision_mapping(MarketCase item)
    {
        var data = new S8010SceneData(
            CandleCutAlias.Cut8061,
            item.Target,
            item.Mode,
            item.DisplayName,
            new CandleQuoteData(100, 0, 0, ScenePriceDirection.Flat),
            [Row(1, 100, 100, 100, 100, 0, null, null)],
            false,
            false);

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

        Assert.Equal(
            item.ExpectedTitle,
            mutations.OfType<PlayoutSetValue>()
                .Last(mutation => mutation.ObjectName == "title").Value);
        if (item.ExpectedUnit is null)
        {
            Assert.DoesNotContain(
                mutations.OfType<PlayoutSetValue>(),
                mutation => mutation.ObjectName == "unit");
        }
        else
        {
            Assert.Equal(
                item.ExpectedUnit,
                mutations.OfType<PlayoutSetValue>()
                    .Last(mutation => mutation.ObjectName == "unit").Value);
        }
        Assert.Contains(mutations, mutation =>
            mutation is PlayoutSetValue { ObjectName: "price", Value: var value } &&
            value == item.ExpectedPrice);
    }

    [Theory]
    [InlineData(CandleCutAlias.Cut8035, 78, false, false)]
    [InlineData(CandleCutAlias.Cut8061, 5, true, false)]
    [InlineData(CandleCutAlias.Cut8040, 20, true, true)]
    [InlineData(CandleCutAlias.Cut8046, 60, true, true)]
    [InlineData(CandleCutAlias.Cut8051, 120, true, true)]
    [InlineData(CandleCutAlias.Cut8056, 240, true, true)]
    public void Alias_asset_profiles_bound_object_names_and_optional_paths(
        CandleCutAlias alias,
        int capacity,
        bool supportsMovingAveragePaths,
        bool supportsGuide)
    {
        var data = new S8010SceneData(
            alias,
            CandleMarketTarget.KospiStock,
            CandleChartMode.Price,
            "Stock",
            new CandleQuoteData(100, 0, 0, ScenePriceDirection.Flat),
            [Row(1, 100, 110, 90, 100, 0, 100, 100, new TimeOnly(9, 0))],
            ShowMovingAverage5: true,
            ShowMovingAverage20: true);

        var mutations = new S8010SceneMutationBuilder().Build(data);
        Assert.Equal(capacity, CandleCutAliasMetadata.GetAssetObjectCapacity(alias));
        Assert.Equal(
            capacity,
            mutations.OfType<PlayoutSetVisible>()
                .Count(item => item.ObjectName.StartsWith("volume", StringComparison.Ordinal) &&
                    !item.IsVisible));
        Assert.Equal(
            capacity,
            mutations.OfType<PlayoutSetVisible>()
                .Count(item => item.ObjectName.StartsWith("candle", StringComparison.Ordinal) &&
                    !item.IsVisible));
        Assert.Equal(
            supportsMovingAveragePaths,
            mutations.OfType<PlayoutSetPathPoints>().Any());
        if (!supportsMovingAveragePaths)
        {
            Assert.Contains(mutations, mutation =>
                mutation is PlayoutSetVisible { ObjectName: "ma5", IsVisible: false });
            Assert.Contains(mutations, mutation =>
                mutation is PlayoutSetVisible { ObjectName: "ma20", IsVisible: false });
        }
        Assert.Equal(
            supportsGuide,
            mutations.OfType<PlayoutSetVisible>().Any(item => item.ObjectName == "guide"));
    }

    [Theory]
    [InlineData(CandleMarketTarget.KospiIndex)]
    [InlineData(CandleMarketTarget.KosdaqIndex)]
    [InlineData(CandleMarketTarget.Kospi200Index)]
    [InlineData(CandleMarketTarget.Krx100Index)]
    [InlineData(CandleMarketTarget.FuturesIndex)]
    [InlineData(CandleMarketTarget.KospiIndustry)]
    [InlineData(CandleMarketTarget.KosdaqIndustry)]
    [InlineData(CandleMarketTarget.OverseasIndex)]
    public void IndexAndIndustryTargetsForceMovingAveragePathsAndGuideOff(
        CandleMarketTarget target)
    {
        var data = new S8010SceneData(
            CandleCutAlias.Cut8040,
            target,
            CandleChartMode.Price,
            "Target",
            new CandleQuoteData(100, 0, 0, ScenePriceDirection.Flat),
            [Row(1, 100, 110, 90, 100, 10, 101, 102)],
            ShowMovingAverage5: true,
            ShowMovingAverage20: true);

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

        Assert.Empty(mutations.OfType<PlayoutSetPathPoints>()
            .Last(path => path.ObjectName == "ma5").Points);
        Assert.Empty(mutations.OfType<PlayoutSetPathPoints>()
            .Last(path => path.ObjectName == "ma20").Points);
        Assert.False(mutations.OfType<PlayoutSetVisible>()
            .Last(visible => visible.ObjectName == "guide").IsVisible);
    }

    [Fact]
    public void Equal_range_zero_volume_and_single_partial_path_are_finite()
    {
        var data = new S8010SceneData(
            CandleCutAlias.Cut8040,
            CandleMarketTarget.KospiStock,
            CandleChartMode.PriceWithVolume,
            "Equal",
            new CandleQuoteData(100, 0, 0, ScenePriceDirection.Flat),
            [Row(1, 100, 100, 100, 100, 0, 100, 100)],
            true,
            true);

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

        Assert.All(mutations, AssertFinite);
        Assert.Contains(mutations, mutation =>
            mutation is PlayoutSetCropKey
            {
                ObjectName: "candlebody1",
                Top: 0,
                Bottom: 1
            });
        Assert.Contains(mutations, mutation =>
            mutation is PlayoutSetCropKey
            {
                ObjectName: "volume1",
                Top: 100,
                Bottom: 100
            });
        var ma5 = mutations.OfType<PlayoutSetPathPoints>()
            .Last(path => path.ObjectName == "ma5");
        var point = Assert.Single(ma5.Points);
        Assert.Equal(0f, point.X);
        Assert.Equal(98f, point.Y);
    }

    [Fact]
    public void Invalid_alias_target_ohlc_volume_time_and_count_fail_closed()
    {
        var valid = ValidData();
        Assert.Throws<LegacySceneDataException>(() =>
            new S8010SceneMutationBuilder().Build(valid with { CutAlias = (CandleCutAlias)9999 }));
        Assert.Throws<LegacySceneDataException>(() =>
            new S8010SceneMutationBuilder().Build(valid with { MarketTarget = (CandleMarketTarget)99 }));
        Assert.Throws<LegacySceneDataException>(() =>
            new S8010SceneMutationBuilder().Build(valid with { ChartMode = (CandleChartMode)99 }));
        Assert.Throws<LegacySceneDataException>(() =>
            new S8010SceneMutationBuilder().Build(valid with { Rows = [] }));
        Assert.Throws<LegacySceneDataException>(() =>
            new S8010SceneMutationBuilder().Build(valid with
            {
                Rows = Enumerable.Range(1, 241)
                    .Select(index => Row(((index - 1) % 28) + 1, 1, 1, 1, 1, 0, null, null))
                    .ToArray()
            }));
        Assert.Throws<LegacySceneDataException>(() =>
            new S8010SceneMutationBuilder().Build(valid with
            {
                Rows = [Row(1, 100, 90, 95, 100, 0, null, null)]
            }));
        Assert.Throws<LegacySceneDataException>(() =>
            new S8010SceneMutationBuilder().Build(valid with
            {
                Rows = [Row(1, 100, 110, 90, 100, -1, null, null)]
            }));
        Assert.Throws<LegacySceneDataException>(() =>
            new S8010SceneMutationBuilder().Build(valid with
            {
                CutAlias = CandleCutAlias.Cut8035,
                Rows = [Row(1, 100, 110, 90, 100, 0, null, null, null)]
            }));
        Assert.Throws<LegacySceneDataException>(() =>
            new S8010SceneMutationBuilder().Build(valid with
            {
                ChartMode = CandleChartMode.TradingHalt,
                MarketTarget = CandleMarketTarget.KospiIndex
            }));
        Assert.Throws<LegacySceneDataException>(() =>
            new S8010SceneMutationBuilder().Build(valid with
            {
                Quote = valid.Quote with { CurrentPrice = double.NaN }
            }));
        Assert.Throws<LegacySceneDataException>(() =>
            new S8010SceneMutationBuilder().Build(valid with
            {
                Rows = [Row(1, 0, double.MaxValue, -double.MaxValue, 0, 0, null, null)]
            }));
        Assert.Throws<LegacySceneDataException>(() =>
            new S8010SceneMutationBuilder().Build(valid with
            {
                Rows = [Row(1, 100, 110, 90, 100, 0, double.PositiveInfinity, null)]
            }));

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

    private static S8010SceneData ValidData() => new(
        CandleCutAlias.Cut8061,
        CandleMarketTarget.KospiStock,
        CandleChartMode.Price,
        "A",
        new CandleQuoteData(100, 0, 0, ScenePriceDirection.Flat),
        [Row(1, 100, 110, 90, 100, 0, null, null)],
        false,
        false);

    private static CandleRowData Row(
        int day,
        double open,
        double high,
        double low,
        double close,
        double? volume,
        double? ma5,
        double? ma20,
        TimeOnly? time = null) =>
        new(
            new DateOnly(2026, 7, day),
            time,
            open,
            high,
            low,
            close,
            volume,
            ma5,
            ma20);

    private static void AssertFinite(PlayoutMutation mutation)
    {
        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 PlayoutSetCropKey crop:
                Assert.True(float.IsFinite(crop.Left));
                Assert.True(float.IsFinite(crop.Top));
                Assert.True(float.IsFinite(crop.Right));
                Assert.True(float.IsFinite(crop.Bottom));
                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;
        }
    }

    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}",
        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}"),
        PlayoutSetCropKey crop => FormattableString.Invariant(
            $"K:{crop.ObjectName}:{crop.KeyIndex}:{crop.Left}:{crop.Top}:{crop.Right}:{crop.Bottom}:{(int)crop.Edges}"),
        PlayoutSetPathPoints path => "L:" + path.ObjectName + "=" + string.Join(
            ";",
            path.Points.Select(point => FormattableString.Invariant($"{point.X},{point.Y},{point.Z}"))),
        _ => mutation.GetType().Name
    };

    public sealed record MarketCase(
        CandleMarketTarget Target,
        CandleChartMode Mode,
        string DisplayName,
        string ExpectedTitle,
        string? ExpectedUnit,
        string ExpectedPrice);
}
