using MBN_STOCK_WEBVIEW.Core.Playout;
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;

namespace MBN_STOCK_WEBVIEW.Core.Tests;

public sealed class RegisteredLegacySceneCueProviderTests
{
    [Fact]
    public async Task CreatePage_DefaultFadeMatchesLegacyMainFormSelectedIndexSix()
    {
        var provider = new RegisteredLegacySceneCueProvider(
            new StubDataSource(new LegacySceneDataPage(
                "s5085",
                new S5085SceneData(
                [
                    new ProgramTradingMetricData(ProgramTradingMetric.NetBuy, 0d)
                ]),
                1)));

        var page = await provider.CreatePageAsync(new("entry", "5085"), 0);

        Assert.Equal(6, page.Cue.FadeDuration);
    }

    [Fact]
    public async Task CreatePage_ComposesLegacyBackgroundThenTypedBuilderMutations()
    {
        var data = new S5085SceneData(
        [
            new ProgramTradingMetricData(ProgramTradingMetric.NetBuy, 12.5)
        ]);
        var provider = new RegisteredLegacySceneCueProvider(
            new StubDataSource(new LegacySceneDataPage("s5085", data, 1)),
            options: new LegacySceneCueCompositionOptions(
                3,
                LegacySceneBackgroundKind.Texture,
                "Video\\studio.vrv"));

        var page = await provider.CreatePageAsync(new("entry", "5085", FadeDuration: 7), 0);

        Assert.Equal("5085.t2s", page.Cue.SceneFile);
        Assert.Equal("5085", page.Cue.SceneName);
        Assert.Equal(7, page.Cue.FadeDuration);
        Assert.Null(page.PageSize);
        Assert.Equal(1, page.ItemCount);
        Assert.Collection(
            page.Cue.Mutations!.Take(2),
            mutation => Assert.Equal(
                new PlayoutSetBackgroundTexture("Video\\studio.vrv"),
                mutation),
            mutation => Assert.Equal(new PlayoutUseBackground(true), mutation));
        Assert.Equal(
            new S5085SceneMutationBuilder().Build(data),
            page.Cue.Mutations!.Skip(2));
    }

    [Fact]
    public async Task CreatePage_8032_omits_only_objects_absent_from_its_authoritative_asset()
    {
        var data = new S8018MarketPairSceneData(
            LegacyMarketQuoteTarget.Kosdaq,
            LegacyMarketQuoteTarget.Kospi,
            new S8018MarketQuote("코스닥", 866.72m, -62.63m, -6.74m, ScenePriceDirection.Down),
            new S8018MarketQuote("코스피", 7_648.09m, -655.32m, -7.89m, ScenePriceDirection.Down));
        var provider = new RegisteredLegacySceneCueProvider(
            new StubDataSource(new LegacySceneDataPage("s8018", data, 1)));

        var page = await provider.CreatePageAsync(new("entry", "8032"), 0);
        var mutations = page.Cue.Mutations!;

        Assert.Contains(new PlayoutSetValue("title1", "코스닥"), mutations);
        Assert.Contains(new PlayoutSetValue("price2", "7,648.09"), mutations);
        Assert.Contains(new PlayoutSetValue("unitText1", "단위 : p"), mutations);
        Assert.DoesNotContain(mutations, mutation => mutation switch
        {
            PlayoutSetAssetValue { ObjectName: "pattern1" or "pattern2" } => true,
            PlayoutSetValue { ObjectName: "wonText1" or "wonText2" } => true,
            PlayoutSetVisible visible => visible.ObjectName.StartsWith("percent", StringComparison.Ordinal) ||
                visible.ObjectName.StartsWith("wonText", StringComparison.Ordinal),
            _ => false
        });
    }

    [Fact]
    public async Task CreatePage_5032_omits_only_decorations_absent_from_its_authoritative_asset()
    {
        var data = new S5032IndexAndFuturesSceneData(
            LegacyMarketQuoteTarget.Kospi,
            LegacyMarketQuoteTarget.Futures,
            new S5032PrimaryQuote("KOSPI", 7_648.09m, -655.32m, -7.89m, ScenePriceDirection.Down),
            new S5032CompanionQuote(
                "Futures",
                1_240.9m,
                -102.35m,
                S5032CompanionSignal.Negative,
                153_907m,
                54_477_919m,
                171_636m));
        var provider = new RegisteredLegacySceneCueProvider(
            new StubDataSource(new LegacySceneDataPage("s5032", data, 1)));

        var page = await provider.CreatePageAsync(new("entry", "5032"), 0);
        var mutations = page.Cue.Mutations!;

        Assert.Contains(new PlayoutSetValue("title", "KOSPI"), mutations);
        Assert.Contains(new PlayoutSetValue("price", "7,648.09"), mutations);
        Assert.Contains(new PlayoutSetValue("title1", "Futures"), mutations);
        Assert.Contains(new PlayoutSetValue("r_value3", "171,636"), mutations);
        Assert.DoesNotContain(mutations, mutation => mutation switch
        {
            PlayoutSetAssetValue { ObjectName: "pattern" } => true,
            PlayoutSetVisible visible => visible.ObjectName.EndsWith('_'),
            _ => false
        });
    }

    [Fact]
    public async Task CreatePage_CommonVideoBackgroundUsesOriginalRawK3dLoopValueTen()
    {
        var provider = new RegisteredLegacySceneCueProvider(
            new StubDataSource(new LegacySceneDataPage(
                "s5085",
                new S5085SceneData(
                [
                    new ProgramTradingMetricData(ProgramTradingMetric.NetBuy, 0d)
                ]),
                1)),
            options: new LegacySceneCueCompositionOptions(
                6,
                LegacySceneBackgroundKind.Video,
                "Video\\studio.vrv"));

        var page = await provider.CreatePageAsync(new("entry", "5085"), 0);

        Assert.Equal(
            new PlayoutSetBackgroundVideo("Video\\studio.vrv", 2004, 10),
            page.Cue.Mutations![0]);
        Assert.Equal(new PlayoutUseBackground(true), page.Cue.Mutations[1]);
    }

    [Fact]
    public async Task CreatePagePlan_UsesDedicatedDtoRouteWithoutBuildingACue()
    {
        var rows = Enumerable.Range(1, 101)
            .Select(index => new PagedQuoteRowData(
                $"item-{index}",
                index,
                index,
                index,
                ScenePriceDirection.Up))
            .ToArray();
        var page = new LegacySceneDataPage(
            "s5074",
            new PagedQuoteSceneData(
                PagedQuoteBranch.Domestic,
                PagedQuoteHeaderStyle.StandardWon,
                PagedQuoteTitleStyle.Exact,
                NxtMarketSession.NotApplicable,
                "preflight",
                rows,
                PageNumberOneBased: 1),
            rows.Length);
        var dataSource = new PagePlanDataSource(page);
        var provider = new RegisteredLegacySceneCueProvider(dataSource);

        var plan = await provider.CreatePagePlanAsync(new("entry", "5074"));

        Assert.Equal("s5074", plan.BuilderKey);
        Assert.Equal(101, plan.ItemCount);
        Assert.Equal(ScenePageSize.Five, plan.PageSize);
        Assert.Equal(20, plan.PageCount);
        Assert.Equal(100, plan.AccessibleItemCount);
        Assert.True(plan.IsTruncated);
        Assert.Equal(0, dataSource.PageCalls);
        Assert.Equal(1, dataSource.PagePlanCalls);
    }

    [Fact]
    public async Task CreatePagePlan_PreservesAnExplicitZeroItemResult()
    {
        var page = new LegacySceneDataPage(
            "s5077",
            new PagedQuoteSceneData(
                PagedQuoteBranch.Domestic,
                PagedQuoteHeaderStyle.StandardWon,
                PagedQuoteTitleStyle.Exact,
                NxtMarketSession.NotApplicable,
                "empty",
                [],
                PageNumberOneBased: 1),
            0);
        var dataSource = new PagePlanDataSource(page);
        var provider = new RegisteredLegacySceneCueProvider(dataSource);

        var plan = await provider.CreatePagePlanAsync(new("empty-entry", "5077"));

        Assert.Equal("s5077", plan.BuilderKey);
        Assert.Equal(0, plan.ItemCount);
        Assert.Equal(ScenePageSize.Six, plan.PageSize);
        Assert.Equal(0, plan.PageCount);
        Assert.Equal(0, plan.AccessibleItemCount);
        Assert.False(plan.IsTruncated);
        Assert.Equal(0, dataSource.PageCalls);
        Assert.Equal(1, dataSource.PagePlanCalls);
    }

    [Fact]
    public async Task MutableCompositionSource_AppliesOnlyToCuesStartedAfterTheUpdate()
    {
        var pageData = new LegacySceneDataPage(
            "s5085",
            new S5085SceneData(
            [
                new ProgramTradingMetricData(ProgramTradingMetric.NetBuy, 12.5)
            ]),
            1);
        var dataSource = new BlockingDataSource(pageData);
        var optionsSource = new MutableLegacySceneCueCompositionOptionsSource(
            LegacySceneCueCompositionOptions.Default);
        var provider = new RegisteredLegacySceneCueProvider(dataSource, optionsSource);

        var inFlight = provider.CreatePageAsync(new("entry-one", "5085"), 0);
        await dataSource.Started;
        optionsSource.Update(new LegacySceneCueCompositionOptions(
            6,
            LegacySceneBackgroundKind.Texture,
            "Video\\studio.png"));
        dataSource.Release();

        var first = await inFlight;
        var second = await provider.CreatePageAsync(new("entry-two", "5085"), 0);

        Assert.Equal(new PlayoutUseBackground(false), first.Cue.Mutations![0]);
        Assert.Collection(
            second.Cue.Mutations!.Take(2),
            mutation => Assert.Equal(
                new PlayoutSetBackgroundTexture("Video\\studio.png"),
                mutation),
            mutation => Assert.Equal(new PlayoutUseBackground(true), mutation));
    }

    [Fact]
    public void MutableCompositionSource_RejectsUnsafeReplacementAndKeepsTheCurrentSnapshot()
    {
        var source = new MutableLegacySceneCueCompositionOptionsSource(
            LegacySceneCueCompositionOptions.Default);

        Assert.Throws<ArgumentException>(() => source.Update(
            new LegacySceneCueCompositionOptions(
                6,
                LegacySceneBackgroundKind.Video,
                "..\\outside.vrv")));
        Assert.Equal(LegacySceneCueCompositionOptions.Default, source.Current);
    }

    [Fact]
    public async Task CreatePage_UsesCatalogPageSizeAndKeepsTheFullItemCount()
    {
        var data = new PagedQuoteSceneData(
            PagedQuoteBranch.Domestic,
            PagedQuoteHeaderStyle.StandardWon,
            PagedQuoteTitleStyle.Exact,
            NxtMarketSession.NotApplicable,
            "상승률",
            Enumerable.Range(1, 101)
                .Select(index => new PagedQuoteRowData(
                    "종목" + index,
                    index,
                    index,
                    index,
                    ScenePriceDirection.Up))
                .ToArray(),
            PageNumberOneBased: 20);
        var provider = new RegisteredLegacySceneCueProvider(
            new StubDataSource(new LegacySceneDataPage("s5074", data, 101)));

        var page = await provider.CreatePageAsync(new("entry", "5074"), 19);

        Assert.Equal(ScenePageSize.Five, page.PageSize);
        Assert.Equal(101, page.ItemCount);
        Assert.Equal(new PlayoutUseBackground(false), page.Cue.Mutations![0]);
        Assert.Contains(page.Cue.Mutations, mutation =>
            mutation is PlayoutSetValue { ObjectName: "title1", Value: "종목96" });
    }

    [Theory]
    [InlineData("5085", "s8086")]
    [InlineData("5085", "s5001")]
    [InlineData("5001", "s8018")]
    public async Task CreatePage_RejectsUnreachableOrMismatchedBuilderAliases(
        string cutCode,
        string builderKey)
    {
        var data = new S5085SceneData([]);
        var provider = new RegisteredLegacySceneCueProvider(
            new StubDataSource(new LegacySceneDataPage(builderKey, data, 1)));

        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            provider.CreatePageAsync(new("entry", cutCode), 0));
    }

    [Theory]
    [InlineData("../outside.vrv")]
    [InlineData("C:\\outside.vrv")]
    [InlineData("")]
    public void Constructor_RejectsUnsafeBackgroundConfiguration(string path)
    {
        Assert.Throws<ArgumentException>(() => new RegisteredLegacySceneCueProvider(
            new StubDataSource(new LegacySceneDataPage("s5085", new S5085SceneData([]), 1)),
            options: new LegacySceneCueCompositionOptions(
                0,
                LegacySceneBackgroundKind.Video,
                path)));
    }

    [Fact]
    public async Task CreatePage_RejectsPage21BeforeBuildingOrDispatching()
    {
        var data = new PagedQuoteSceneData(
            PagedQuoteBranch.Domestic,
            PagedQuoteHeaderStyle.StandardWon,
            PagedQuoteTitleStyle.Exact,
            NxtMarketSession.NotApplicable,
            "상승률",
            Enumerable.Range(1, 101)
                .Select(index => new PagedQuoteRowData(
                    "종목" + index,
                    index,
                    index,
                    index,
                    ScenePriceDirection.Up))
                .ToArray(),
            PageNumberOneBased: 21);
        var provider = new RegisteredLegacySceneCueProvider(
            new StubDataSource(new LegacySceneDataPage("s5074", data, 101)));

        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            provider.CreatePageAsync(new("entry", "5074"), 20));
    }

    private sealed class StubDataSource(LegacySceneDataPage page) : ILegacySceneDataSource
    {
        public Task<LegacySceneDataPage> LoadPageAsync(
            LegacyPlaylistEntry entry,
            int pageIndexZeroBased,
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            return Task.FromResult(page);
        }
    }

    private sealed class PagePlanDataSource(LegacySceneDataPage planPage) :
        ILegacySceneDataSource,
        ILegacyScenePagePlanDataSource
    {
        public int PageCalls { get; private set; }

        public int PagePlanCalls { get; private set; }

        public Task<LegacySceneDataPage> LoadPageAsync(
            LegacyPlaylistEntry entry,
            int pageIndexZeroBased,
            CancellationToken cancellationToken = default)
        {
            PageCalls++;
            throw new InvalidOperationException("Normal cue creation must not run during preflight.");
        }

        public Task<LegacySceneDataPage> LoadPagePlanDataAsync(
            LegacyPlaylistEntry entry,
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            PagePlanCalls++;
            return Task.FromResult(planPage);
        }
    }

    private sealed class BlockingDataSource(LegacySceneDataPage page) : ILegacySceneDataSource
    {
        private readonly TaskCompletionSource _started = new(
            TaskCreationOptions.RunContinuationsAsynchronously);
        private TaskCompletionSource _release = new(
            TaskCreationOptions.RunContinuationsAsynchronously);

        public Task Started => _started.Task;

        public void Release() => _release.TrySetResult();

        public async Task<LegacySceneDataPage> LoadPageAsync(
            LegacyPlaylistEntry entry,
            int pageIndexZeroBased,
            CancellationToken cancellationToken = default)
        {
            _started.TrySetResult();
            await _release.Task.WaitAsync(cancellationToken);
            _release = new TaskCompletionSource(
                TaskCreationOptions.RunContinuationsAsynchronously);
            _release.TrySetResult();
            return page;
        }
    }
}
