using MBN_STOCK_WEBVIEW.Core.Playout;
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;
using MMoneyCoderSharp.Data;

namespace MBN_STOCK_WEBVIEW.LegacyApplication.Tests;

public sealed class LegacyNamedPlaylistOperatorIntegrationTests
{
    [Fact]
    public void Construction_DoesNotReadOrWriteNamedPlaylistDatabase()
    {
        var persistence = new OperatorNamedPlaylistPersistence([]);

        _ = CreateController(persistence);

        Assert.Equal(0, persistence.ListCalls);
        Assert.Equal(0, persistence.LoadCalls);
        Assert.Equal(0, persistence.ReplaceCalls);
        Assert.Equal(0, persistence.CreateCalls);
        Assert.Equal(0, persistence.DeleteCalls);
    }

    [Fact]
    public async Task Load_by_opaque_id_selects_and_loads_in_one_native_command()
    {
        var source = LegacyStockCutCatalog.CreatePlaylistRow(
            "source-row",
            "Samsung Electronics",
            new LegacyStockIdentity(
                LegacyDomesticStockMarket.Kospi,
                "Samsung Electronics",
                "005930"),
            LegacyStockCutCatalog.Rows[1]);
        var selection = Assert.IsType<LegacySceneSelection>(
            source.ToPlayoutEntry().Selection);
        var persistence = new OperatorNamedPlaylistPersistence(
        [
            new NamedPlaylistStoredItem(
                0,
                true,
                selection,
                new NamedPlaylistPageState(1, 1))
        ]);
        var controller = CreateController(persistence);
        var listed = await controller.RefreshNamedPlaylistsAsync();
        var definitionId = Assert.Single(
            listed.NamedPlaylist!.Definitions).DefinitionId;

        var loaded = await controller.LoadNamedPlaylistByIdAsync(definitionId);

        Assert.Equal(1, persistence.LoadCalls);
        Assert.Single(loaded.Playlist);
        Assert.Equal("load-named-playlist-by-id", loaded.CommandResult?.Command);
        Assert.Equal(definitionId, loaded.CommandResult?.TargetId);
        Assert.True(loaded.CommandResult?.Succeeded);
        Assert.Equal(LegacyNamedPlaylistFreshness.Fresh,
            loaded.NamedPlaylist?.DocumentFreshness);
    }

    [Theory]
    [InlineData("fixed-047")]
    [InlineData("fixed-044")]
    [InlineData("fixed-076")]
    [InlineData("fixed-063")]
    [InlineData("fixed-085")]
    [InlineData("fixed-105")]
    [InlineData("fixed-234")]
    [InlineData("fixed-236")]
    [InlineData("fixed-275")]
    [InlineData("fixed-233")]
    [InlineData("fixed-240")]
    [InlineData("fixed-235")]
    [InlineData("fixed-244")]
    [InlineData("fixed-302")]
    [InlineData("fixed-243")]
    [InlineData("fixed-136")]
    public async Task Save_and_load_round_trip_restores_fixed_playout_selection(
        string actionId)
    {
        var persistence = new OperatorNamedPlaylistPersistence([]);
        var controller = CreateController(
            persistence,
            new FixedTimeProvider(
                new DateTimeOffset(2026, 7, 22, 10, 0, 0, TimeSpan.FromHours(9))));
        var action = LegacyFixedActionCatalog.Default.GetAction(actionId);
        controller.SelectTab(action.Market switch
        {
            LegacyFixedMarket.Overseas => LegacyOperatorTabId.Overseas,
            LegacyFixedMarket.Exchange => LegacyOperatorTabId.Exchange,
            LegacyFixedMarket.Index => LegacyOperatorTabId.Index,
            _ => throw new ArgumentOutOfRangeException()
        });
        controller.ActivateFixedAction(actionId);
        var before = Assert.Single(controller.CreatePlayoutRequest().Playlist);
        var listed = await controller.RefreshNamedPlaylistsAsync();
        var definitionId = Assert.Single(
            listed.NamedPlaylist!.Definitions).DefinitionId;
        controller.SelectNamedPlaylist(definitionId);
        await controller.SaveCurrentNamedPlaylistAsync();

        var loaded = await controller.LoadNamedPlaylistByIdAsync(definitionId);
        var after = Assert.Single(controller.CreatePlayoutRequest().Playlist);

        Assert.True(loaded.CommandResult?.Succeeded);
        Assert.Equal(before.CutCode, after.CutCode);
        Assert.Equal(before.Selection, after.Selection);
    }

    [Fact]
    public async Task SaveFixed8035PersistsOriginalIndexSevenFields()
    {
        var persistence = new OperatorNamedPlaylistPersistence([]);
        var controller = CreateController(persistence);
        controller.SelectTab(LegacyOperatorTabId.Index);
        controller.ActivateFixedAction("fixed-136");
        var listed = await controller.RefreshNamedPlaylistsAsync();
        var definitionId = Assert.Single(
            listed.NamedPlaylist!.Definitions).DefinitionId;
        controller.SelectNamedPlaylist(definitionId);

        var saved = await controller.SaveCurrentNamedPlaylistAsync();

        Assert.Equal(
            LegacyNamedPlaylistMutationOutcome.CommittedFresh,
            saved.NamedPlaylist?.LastMutationOutcome);
        var stored = Assert.Single(persistence.LastReplaceItems!);
        Assert.True(stored.IsEnabled);
        Assert.Equal("코스피", stored.Selection.GroupCode);
        Assert.Equal("지수", stored.Selection.Subject);
        Assert.Equal("캔들그래프", stored.Selection.GraphicType);
        Assert.Equal("일봉", stored.Selection.Subtype);
        Assert.Equal(string.Empty, stored.Selection.DataCode);
        Assert.Equal(new NamedPlaylistPageState(1, 1), stored.Page);
    }

    [Fact]
    public async Task SaveFixedPagedActionPersistsInitialDatabasePageNBeforePlayout()
    {
        var persistence = new OperatorNamedPlaylistPersistence([]);
        var controller = CreateController(
            persistence,
            fixedPagePlanProvider: new FixedSixRowPagePlanProvider());
        controller.SelectTab(LegacyOperatorTabId.Index);
        var action = LegacyFixedActionCatalog.Default.Actions
            .First(candidate => candidate.Available && candidate.Code == "5077");

        var added = await controller.ActivateFixedActionAsync(action.Id);
        Assert.Equal("1/3", Assert.Single(added.Playlist).PageText);
        var listed = await controller.RefreshNamedPlaylistsAsync();
        controller.SelectNamedPlaylist(
            Assert.Single(listed.NamedPlaylist!.Definitions).DefinitionId);

        await controller.SaveCurrentNamedPlaylistAsync();

        Assert.Equal(
            new NamedPlaylistPageState(1, 3),
            Assert.Single(persistence.LastReplaceItems!).Page);
    }

    [Theory]
    [InlineData("코스피", "지수", "캔들그래프", "일봉")]
    [InlineData("주요지수", "캔들그래프_일봉", "코스피", "코스피")]
    public async Task LoadFixed8035RestoresOriginalAndCurrentStoredShapes(
        string groupCode,
        string subject,
        string graphicType,
        string subtype)
    {
        var persistence = new OperatorNamedPlaylistPersistence(
        [
            new NamedPlaylistStoredItem(
                0,
                true,
                new LegacySceneSelection(
                    groupCode,
                    subject,
                    graphicType,
                    subtype,
                    string.Empty),
                new NamedPlaylistPageState(1, 1))
        ]);
        var controller = CreateController(persistence);
        var listed = await controller.RefreshNamedPlaylistsAsync();

        var loaded = await controller.LoadNamedPlaylistByIdAsync(
            Assert.Single(listed.NamedPlaylist!.Definitions).DefinitionId);

        Assert.True(loaded.CommandResult?.Succeeded);
        Assert.Null(loaded.Dialog);
        var row = Assert.Single(loaded.Playlist);
        Assert.True(row.IsSceneResolved);
        Assert.Equal(groupCode, row.MarketText);
        Assert.Equal(subject, row.StockName);
        Assert.Equal(graphicType, row.GraphicType);
        Assert.Equal(subtype, row.Subtype);
        Assert.Equal("1/1", row.PageText);
        Assert.Equal(string.Empty, row.StockCode);
        Assert.Equal("8035", row.SceneAlias);
        Assert.Equal(
            new LegacySceneSelection(
                "KOSPI_INDEX",
                "INDEX",
                string.Empty,
                "PRICE",
                string.Empty),
            row.PlayoutSelection);
    }

    [Fact]
    public async Task LoadSelected_ReplacesPlaylistWithTypedOriginalSevenFields()
    {
        var persistence = new OperatorNamedPlaylistPersistence(
        [
            new NamedPlaylistStoredItem(
                0,
                false,
                new LegacySceneSelection(
                    "코스피",
                    "삼성전자",
                    "1열판기본",
                    "현재가",
                    "005930"),
                new NamedPlaylistPageState(1, 1))
        ]);
        var controller = CreateController(persistence);
        var listed = await controller.RefreshNamedPlaylistsAsync();
        controller.SelectNamedPlaylist(
            listed.NamedPlaylist!.Definitions.Single().DefinitionId);

        var loaded = await controller.LoadSelectedNamedPlaylistAsync();

        var row = Assert.Single(loaded.Playlist);
        Assert.False(row.IsEnabled);
        Assert.Equal("코스피", row.MarketText);
        Assert.Equal("삼성전자", row.StockName);
        Assert.Equal("1열판기본", row.GraphicType);
        Assert.Equal("현재가", row.Subtype);
        Assert.Equal("1/1", row.PageText);
        Assert.Equal("005930", row.StockCode);
        Assert.Equal("5001", row.SceneAlias);
        Assert.Equal("코스피", row.PlayoutSelection?.GroupCode);
        Assert.True(row.IsActive);
        Assert.False(row.IsSelected);
        Assert.Equal(0, controller.CreatePlayoutRequest().SelectedIndexZeroBased);
    }

    [Fact]
    public async Task LoadSelected_UnknownSceneLoadsRawRowAndDefersOnlyThatRowsPlayoutCheck()
    {
        var persistence = new OperatorNamedPlaylistPersistence(
        [
            new NamedPlaylistStoredItem(
                0,
                true,
                new LegacySceneSelection(
                    "UNKNOWN-GROUP",
                    "알 수 없음",
                    "UNKNOWN-GRAPHIC",
                    "UNKNOWN-SUBTYPE",
                    "UNKNOWN-DATA"),
                null)
        ]);
        var controller = CreateController(persistence);
        var fixedAction = LegacyFixedActionCatalog.Default
            .GetActions(LegacyFixedMarket.Overseas)
            .First(action => action.Available);
        controller.ActivateFixedAction(fixedAction.Id);
        var before = controller.Current.Playlist.Single();
        var listed = await controller.RefreshNamedPlaylistsAsync();
        var definitionId = listed.NamedPlaylist!.Definitions.Single().DefinitionId;

        var after = await controller.LoadNamedPlaylistByIdAsync(definitionId);

        var row = Assert.Single(after.Playlist);
        Assert.NotEqual(before.RowId, row.RowId);
        Assert.Equal("UNKNOWN-GROUP", row.MarketText);
        Assert.Equal("알 수 없음", row.StockName);
        Assert.Equal("UNKNOWN-GRAPHIC", row.GraphicType);
        Assert.Equal("UNKNOWN-SUBTYPE", row.Subtype);
        Assert.Equal("UNKNOWN-DATA", row.StockCode);
        Assert.Equal(LegacyPlaylistEntry.UnresolvedCutCode, row.SceneAlias);
        Assert.False(row.IsSceneResolved);
        Assert.Null(after.Dialog);
        Assert.Equal(LegacyOperatorStatusKind.Warning, after.StatusKind);
        Assert.Equal("load-named-playlist-by-id", after.CommandResult?.Command);
        Assert.True(after.CommandResult?.Succeeded);
        Assert.Equal(
            LegacyPlaylistEntry.UnresolvedCutCode,
            Assert.Single(controller.CreatePlayoutRequest().Playlist).CutCode);
    }

    [Fact]
    public async Task LoadSelected_RestoresOriginalFixedIndexPairFirstRow()
    {
        var persistence = new OperatorNamedPlaylistPersistence(
        [
            new NamedPlaylistStoredItem(
                0,
                true,
                new LegacySceneSelection(
                    "지수",
                    "코스피 지수,코스닥 지수",
                    "2열판",
                    string.Empty,
                    string.Empty),
                new NamedPlaylistPageState(1, 1))
        ]);
        var controller = CreateController(persistence);
        var listed = await controller.RefreshNamedPlaylistsAsync();

        var loaded = await controller.LoadNamedPlaylistByIdAsync(
            Assert.Single(listed.NamedPlaylist!.Definitions).DefinitionId);

        var row = Assert.Single(loaded.Playlist);
        Assert.True(loaded.CommandResult?.Succeeded);
        Assert.Null(loaded.Dialog);
        Assert.True(row.IsSceneResolved);
        Assert.Equal("8032", row.SceneAlias);
        Assert.Equal("INDEX", row.PlayoutSelection?.GroupCode);
        Assert.Equal("Kospi,Kosdaq", row.PlayoutSelection?.Subject);
        Assert.Equal("2열판", row.PlayoutSelection?.GraphicType);
        Assert.Equal("CURRENT", row.PlayoutSelection?.Subtype);
    }

    [Theory]
    [InlineData("INDIVIDUAL", "INDEX", "MANUAL_NET_SELL", "MANUAL_NET_SELL")]
    [InlineData("FOREIGN", "INDEX", "MANUAL_NET_SELL", "MANUAL_NET_SELL")]
    [InlineData("INSTITUTION", "INDEX", "MANUAL_NET_SELL", "MANUAL_NET_SELL")]
    [InlineData("개인 순매도 상위(수동)", "지수", "순매도 상위", "순매도 상위")]
    [InlineData("지수", "개인 순매도 상위(수동)", "순매도 상위", "순매도 상위")]
    public async Task Load_by_opaque_id_restores_trusted_manual_net_sell_alias(
        string groupCode,
        string subject,
        string graphicType,
        string subtype)
    {
        var persistence = new OperatorNamedPlaylistPersistence(
        [
            new NamedPlaylistStoredItem(
                0,
                true,
                new LegacySceneSelection(
                    groupCode,
                    subject,
                    graphicType,
                    subtype,
                    string.Empty),
                new NamedPlaylistPageState(1, 1))
        ]);
        var controller = CreateController(persistence);
        var listed = await controller.RefreshNamedPlaylistsAsync();
        var definitionId = Assert.Single(
            listed.NamedPlaylist!.Definitions).DefinitionId;

        var loaded = await controller.LoadNamedPlaylistByIdAsync(definitionId);

        Assert.True(loaded.CommandResult?.Succeeded);
        Assert.Null(loaded.Dialog);
        var row = Assert.Single(loaded.Playlist);
        Assert.Equal("5025", row.SceneAlias);
        var expectedAudience = groupCode == "FOREIGN" || subject.StartsWith("외국인", StringComparison.Ordinal)
            ? "FOREIGN"
            : groupCode == "INSTITUTION" || subject.StartsWith("기관", StringComparison.Ordinal)
                ? "INSTITUTION"
                : "INDIVIDUAL";
        Assert.Equal(expectedAudience, row.PlayoutSelection?.GroupCode);
        Assert.Equal("INDEX", row.PlayoutSelection?.Subject);
        Assert.Equal("MANUAL_NET_SELL", row.PlayoutSelection?.GraphicType);
    }

    [Fact]
    public async Task Load_by_opaque_id_restores_versioned_vi_alias()
    {
        var persistence = new OperatorNamedPlaylistPersistence(
        [
            new NamedPlaylistStoredItem(
                0,
                true,
                new LegacySceneSelection(
                    "PAGED_VI",
                    TrustedViManualReference.SubjectSentinel,
                    "INPUT_ORDER",
                    "CURRENT",
                    new string('a', 64)),
                new NamedPlaylistPageState(1, 1))
        ]);
        var controller = CreateController(persistence);
        var listed = await controller.RefreshNamedPlaylistsAsync();
        var definitionId = Assert.Single(
            listed.NamedPlaylist!.Definitions).DefinitionId;

        var loaded = await controller.LoadNamedPlaylistByIdAsync(definitionId);

        Assert.True(loaded.CommandResult?.Succeeded);
        Assert.Null(loaded.Dialog);
        Assert.Equal("5074", Assert.Single(loaded.Playlist).SceneAlias);
    }

    [Fact]
    public async Task Load_by_opaque_id_restores_saved_visible_vi_fields_to_versioned_identity()
    {
        var version = new string('b', 64);
        var persistence = new OperatorNamedPlaylistPersistence(
        [
            new NamedPlaylistStoredItem(
                0,
                true,
                new LegacySceneSelection(
                    "지수",
                    "삼성전자,SK하이닉스",
                    "5단 표그래프",
                    "VI 발동(수동)",
                    version),
                new NamedPlaylistPageState(1, 1))
        ]);
        var controller = CreateController(persistence);
        var listed = await controller.RefreshNamedPlaylistsAsync();
        var definitionId = Assert.Single(
            listed.NamedPlaylist!.Definitions).DefinitionId;

        var loaded = await controller.LoadNamedPlaylistByIdAsync(definitionId);

        var row = Assert.Single(loaded.Playlist);
        Assert.True(loaded.CommandResult?.Succeeded);
        Assert.Equal("5074", row.SceneAlias);
        Assert.Equal("PAGED_VI", row.PlayoutSelection?.GroupCode);
        Assert.Equal(TrustedViManualReference.SubjectSentinel,
            row.PlayoutSelection?.Subject);
        Assert.Equal(version, row.PlayoutSelection?.DataCode);
    }

    [Theory]
    [InlineData("액면가", S5011DetailBranch.FaceValue)]
    [InlineData("PBR", S5011DetailBranch.Valuation)]
    [InlineData("거래량", S5011DetailBranch.Volume)]
    public async Task Load_by_opaque_id_restores_saved_stock_detail_branch(
        string visibleSubtype,
        S5011DetailBranch expectedBranch)
    {
        var persistence = new OperatorNamedPlaylistPersistence(
        [
            new NamedPlaylistStoredItem(
                0,
                true,
                new LegacySceneSelection(
                    "코스피",
                    "삼성전자",
                    "1열판상세",
                    visibleSubtype,
                    "005930"),
                new NamedPlaylistPageState(1, 1))
        ]);
        var controller = CreateController(persistence);
        var listed = await controller.RefreshNamedPlaylistsAsync();

        var loaded = await controller.LoadNamedPlaylistByIdAsync(
            Assert.Single(listed.NamedPlaylist!.Definitions).DefinitionId);

        var row = Assert.Single(loaded.Playlist);
        Assert.True(loaded.CommandResult?.Succeeded);
        Assert.Equal("5011", row.SceneAlias);
        Assert.Equal(expectedBranch.ToString(), row.PlayoutSelection?.Subtype);
        Assert.Equal("005930", row.PlayoutSelection?.DataCode);
    }

    [Fact]
    public async Task Load_by_opaque_id_restores_saved_industry_square_selection()
    {
        var persistence = new OperatorNamedPlaylistPersistence(
        [
            new NamedPlaylistStoredItem(
                0,
                true,
                new LegacySceneSelection(
                    "업종_코스피",
                    "코스피 네모그래프",
                    "네모그래프",
                    string.Empty,
                    string.Empty),
                new NamedPlaylistPageState(1, 1))
        ]);
        var controller = CreateController(persistence);
        var listed = await controller.RefreshNamedPlaylistsAsync();

        var loaded = await controller.LoadNamedPlaylistByIdAsync(
            Assert.Single(listed.NamedPlaylist!.Definitions).DefinitionId);

        var row = Assert.Single(loaded.Playlist);
        Assert.True(loaded.CommandResult?.Succeeded);
        Assert.Equal("8001", row.SceneAlias);
        Assert.Equal(
            new LegacySceneSelection(
                "INDUSTRY_KOSPI",
                "KOSPI",
                "네모그래프",
                string.Empty,
                string.Empty),
            row.PlayoutSelection);
    }

    [Fact]
    public async Task Load_by_opaque_id_restores_saved_comparison_selections()
    {
        var stockIdentity = "KOSPI:016360|KOSPI:068290";
        var persistence = new OperatorNamedPlaylistPersistence(
        [
            StoredComparison(0, "s5026", "종목별 비교분석_캔들 그래프", stockIdentity),
            StoredComparison(1, "s5087", "종목별 비교분석_라인 그래프", stockIdentity),
            StoredComparison(2, "s5029", "5일", stockIdentity),
            new NamedPlaylistStoredItem(
                3,
                true,
                new LegacySceneSelection(
                    "비교",
                    "코스닥 지수 ↔ 코스피 지수",
                    "s8018",
                    "2열판",
                    string.Empty),
                new NamedPlaylistPageState(1, 1)),
            new NamedPlaylistStoredItem(
                4,
                true,
                new LegacySceneSelection(
                    "비교",
                    "선물 지수 ↔ 코스피 지수",
                    "s5032",
                    "2열판",
                    string.Empty),
                new NamedPlaylistPageState(1, 1))
        ]);
        var controller = CreateController(persistence);
        var listed = await controller.RefreshNamedPlaylistsAsync();

        var loaded = await controller.LoadNamedPlaylistByIdAsync(
            Assert.Single(listed.NamedPlaylist!.Definitions).DefinitionId);

        Assert.True(loaded.CommandResult?.Succeeded);
        Assert.Equal(
            ["5026", "5087", "5029", "8032", "5032"],
            loaded.Playlist.Select(row => row.SceneAlias));
        Assert.Equal(
            ["COMPARISON_CANDLE", "COMPARISON_LINE", "RETURN_COMPARISON"],
            loaded.Playlist.Take(3).Select(row => row.PlayoutSelection?.GraphicType));
        Assert.All(loaded.Playlist.Take(3), row =>
        {
            Assert.Equal("STOCK", row.PlayoutSelection?.GroupCode);
            Assert.Equal("삼성증권,삼성출판사", row.PlayoutSelection?.Subject);
            Assert.Equal(stockIdentity, row.PlayoutSelection?.DataCode);
        });
        Assert.Equal("Kosdaq,Kospi", loaded.Playlist[3].PlayoutSelection?.Subject);
        Assert.Equal("Futures,Kospi", loaded.Playlist[4].PlayoutSelection?.Subject);
    }

    [Theory]
    [InlineData("KOSDAQ", "APS", "주요매출 구성", "REVENUE_COMPOSITION")]
    [InlineData("KOSPI", "AJ네트웍스", "성장성 지표", "GROWTH_METRICS")]
    [InlineData("KOSDAQ", "3S", "매출액", "SALES")]
    [InlineData("KOSDAQ", "3S", "영업이익", "OPERATING_PROFIT")]
    public async Task Load_by_opaque_id_restores_saved_manual_financial_selection(
        string market,
        string stockName,
        string visibleGraphicType,
        string canonicalGraphicType)
    {
        var persistence = new OperatorNamedPlaylistPersistence(
        [
            new NamedPlaylistStoredItem(
                0,
                true,
                new LegacySceneSelection(
                    market,
                    stockName,
                    visibleGraphicType,
                    canonicalGraphicType,
                    "000001"),
                new NamedPlaylistPageState(1, 1))
        ]);
        var controller = CreateController(persistence);
        var listed = await controller.RefreshNamedPlaylistsAsync();

        var loaded = await controller.LoadNamedPlaylistByIdAsync(
            Assert.Single(listed.NamedPlaylist!.Definitions).DefinitionId);

        var row = Assert.Single(loaded.Playlist);
        Assert.True(loaded.CommandResult?.Succeeded);
        Assert.Equal(canonicalGraphicType, row.PlayoutSelection?.GraphicType);
        Assert.Equal(string.Empty, row.PlayoutSelection?.Subtype);
        Assert.Equal(string.Empty, row.PlayoutSelection?.DataCode);
    }

    private static NamedPlaylistStoredItem StoredComparison(
        int itemIndex,
        string builderKey,
        string detail,
        string dataCode) =>
        new(
            itemIndex,
            true,
            new LegacySceneSelection(
                "비교",
                "삼성증권 ↔ 삼성출판사",
                builderKey,
                detail,
                dataCode),
            new NamedPlaylistPageState(1, 1));

    [Fact]
    public async Task SaveCurrent_WritesVisibleSevenFieldsOnceAndReadsBackFresh()
    {
        var persistence = new OperatorNamedPlaylistPersistence([]);
        var controller = CreateController(persistence);
        var action = LegacyFixedActionCatalog.Default
            .GetActions(LegacyFixedMarket.Overseas)
            .First(candidate => candidate.Available);
        var added = controller.ActivateFixedAction(action.Id);
        var visible = Assert.Single(added.Playlist);
        var listed = await controller.RefreshNamedPlaylistsAsync();
        controller.SelectNamedPlaylist(
            listed.NamedPlaylist!.Definitions.Single().DefinitionId);

        var saved = await controller.SaveCurrentNamedPlaylistAsync();

        Assert.Equal(1, persistence.ReplaceCalls);
        var stored = Assert.Single(persistence.LastReplaceItems!);
        Assert.Equal(0, stored.ItemIndex);
        Assert.Equal(visible.IsEnabled, stored.IsEnabled);
        Assert.Equal(visible.MarketText, stored.Selection.GroupCode);
        Assert.Equal(visible.StockName, stored.Selection.Subject);
        Assert.Equal(visible.GraphicType, stored.Selection.GraphicType);
        Assert.Equal(visible.Subtype, stored.Selection.Subtype);
        Assert.Equal(visible.StockCode, stored.Selection.DataCode);
        Assert.Equal(visible.PageText, stored.Page?.ToString());
        Assert.Equal(
            LegacyNamedPlaylistMutationOutcome.CommittedFresh,
            saved.NamedPlaylist?.LastMutationOutcome);
    }

    [Theory]
    [InlineData(ThemeMarket.Krx, "  Edge Theme  ", "  Edge Theme  ")]
    [InlineData(ThemeMarket.Nxt, "Robotics(NXT)", "Robotics(NXT)(NXT)")]
    [InlineData(ThemeMarket.Nxt, "Robotics(NXT)(NXT)", "Robotics(NXT)(NXT)(NXT)")]
    [InlineData(ThemeMarket.Nxt, "  Edge Theme  ", "  Edge Theme  (NXT)")]
    public async Task Native_theme_schedule_save_fresh_load_retains_exact_database_title(
        ThemeMarket market,
        string exactTitle,
        string expectedStoredSubject)
    {
        var persistence = new OperatorNamedPlaylistPersistence([]);
        var identity = new ThemeSelectionIdentity(
            market,
            market == ThemeMarket.Nxt
                ? ThemeSession.PreMarket
                : ThemeSession.NotApplicable,
            "00000002",
            exactTitle);
        var time = new DateTimeOffset(2026, 7, 21, 10, 0, 0, TimeSpan.FromHours(9));
        var controller = new LegacyOperatorController(
            new UnusedLookup(),
            timeProvider: new FixedTimeProvider(time),
            themeWorkflow: new LegacyThemeWorkflow(
                new ExactThemeService(identity),
                () => time),
            namedPlaylistWorkflow: new LegacyNamedPlaylistWorkflowController(persistence));

        var theme = await controller.SelectTabAsync(LegacyOperatorTabId.Theme);
        await controller.SelectThemeAsync(Assert.Single(theme.Theme!.SearchResults).ResultId);
        controller.ActivateThemeAction("five-row-current");
        var listed = await controller.RefreshNamedPlaylistsAsync();
        var definitionId = Assert.Single(listed.NamedPlaylist!.Definitions).DefinitionId;
        controller.SelectNamedPlaylist(definitionId);

        await controller.SaveCurrentNamedPlaylistAsync();

        var stored = Assert.Single(persistence.LastReplaceItems!);
        Assert.Equal(expectedStoredSubject, stored.Selection.Subject);
        Assert.Equal(market == ThemeMarket.Nxt ? "테마_NXT" : "테마", stored.Selection.GroupCode);
        Assert.Equal("00000002", stored.Selection.DataCode);

        var loaded = await controller.LoadNamedPlaylistByIdAsync(definitionId);
        var runtimeSelection = Assert.Single(
            controller.CreatePlayoutRequest().Playlist).Selection!;
        Assert.True(loaded.CommandResult?.Succeeded);
        Assert.Equal(
            market == ThemeMarket.Nxt ? "PAGED_NXT_THEME" : "PAGED_THEME",
            runtimeSelection.GroupCode);
        Assert.Equal(expectedStoredSubject, runtimeSelection.Subject);
        Assert.Equal(exactTitle, runtimeSelection.ExactSubject);
        Assert.Equal("00000002", runtimeSelection.DataCode);
    }

    [Fact]
    public async Task NamedPlaylistIdentityDoesNotChangeWhenMovingAveragesChange()
    {
        var persistence = new OperatorNamedPlaylistPersistence([]);
        var controller = CreateController(persistence);
        controller.SelectTab(LegacyOperatorTabId.Index);
        var candle = LegacyFixedActionCatalog.Default
            .GetActions(LegacyFixedMarket.Index)
            .First(action => action.SupportsMovingAverages);
        controller.SetMovingAverages(ma5: false, ma20: false);
        var added = controller.ActivateFixedAction(candle.Id);
        var raw = Assert.Single(added.Playlist);
        Assert.Equal(string.Empty, raw.PlayoutSelection!.GraphicType);

        controller.SetMovingAverages(ma5: true, ma20: false);
        Assert.Equal(
            "MA5",
            Assert.Single(controller.CreatePlayoutRequest().Playlist).Selection!.GraphicType);
        var listed = await controller.RefreshNamedPlaylistsAsync();
        controller.SelectNamedPlaylist(
            Assert.Single(listed.NamedPlaylist!.Definitions).DefinitionId);
        await controller.SaveCurrentNamedPlaylistAsync();
        var firstStored = Assert.Single(persistence.LastReplaceItems!).Selection;

        controller.SetMovingAverages(ma5: false, ma20: true);
        Assert.Equal(
            "MA20",
            Assert.Single(controller.CreatePlayoutRequest().Playlist).Selection!.GraphicType);
        await controller.SaveCurrentNamedPlaylistAsync();
        var secondStored = Assert.Single(persistence.LastReplaceItems!).Selection;

        Assert.Equal(firstStored, secondStored);
        Assert.Equal(raw.GraphicType, secondStored.GraphicType);
        Assert.DoesNotContain("MA5", secondStored.GraphicType, StringComparison.Ordinal);
        Assert.DoesNotContain("MA20", secondStored.GraphicType, StringComparison.Ordinal);
        Assert.Equal(string.Empty,
            Assert.Single(controller.Current.Playlist).PlayoutSelection!.GraphicType);
    }

    [Fact]
    public async Task Save_to_opaque_id_selects_and_writes_once_in_one_native_command()
    {
        var persistence = new OperatorNamedPlaylistPersistence([]);
        var controller = CreateController(persistence);
        var action = LegacyFixedActionCatalog.Default
            .GetActions(LegacyFixedMarket.Overseas)
            .First(candidate => candidate.Available);
        controller.ActivateFixedAction(action.Id);
        var listed = await controller.RefreshNamedPlaylistsAsync();
        var definitionId = Assert.Single(
            listed.NamedPlaylist!.Definitions).DefinitionId;

        var saved = await controller.SaveCurrentNamedPlaylistToAsync(definitionId);

        Assert.Equal(1, persistence.ReplaceCalls);
        Assert.Equal("save-current-named-playlist-to", saved.CommandResult?.Command);
        Assert.Equal(definitionId, saved.CommandResult?.TargetId);
        Assert.True(saved.CommandResult?.Succeeded);
        Assert.Null(saved.Dialog);
    }

    [Fact]
    public async Task Confirm_button_save_projects_the_original_saved_MessageBox()
    {
        var persistence = new OperatorNamedPlaylistPersistence([]);
        var controller = CreateController(persistence);
        var listed = await controller.RefreshNamedPlaylistsAsync();
        var definitionId = Assert.Single(
            listed.NamedPlaylist!.Definitions).DefinitionId;

        var saved = await controller.SaveCurrentNamedPlaylistToAsync(
            definitionId,
            showSavedAlert: true);

        Assert.Equal(1, persistence.ReplaceCalls);
        Assert.True(saved.CommandResult?.Succeeded);
        Assert.Equal("저장되었습니다", saved.Dialog?.Message);
        Assert.Equal("MmoneyCoder", saved.Dialog?.Caption);
        Assert.False(saved.Dialog?.IsConfirmation);

        var dismissed = controller.DismissDialog();
        Assert.Null(dismissed.Dialog);
    }

    [Fact]
    public async Task Save_to_opaque_id_treats_known_commit_with_failed_readback_as_success()
    {
        var persistence = new OperatorNamedPlaylistPersistence([])
        {
            FailNextLoad = true
        };
        var controller = CreateController(persistence);
        var listed = await controller.RefreshNamedPlaylistsAsync();
        var definitionId = Assert.Single(
            listed.NamedPlaylist!.Definitions).DefinitionId;

        var saved = await controller.SaveCurrentNamedPlaylistToAsync(definitionId);

        Assert.Equal(1, persistence.ReplaceCalls);
        Assert.Equal(
            LegacyNamedPlaylistMutationOutcome.CommittedOptimistic,
            saved.NamedPlaylist?.LastMutationOutcome);
        Assert.Equal(LegacyOperatorStatusKind.Warning, saved.StatusKind);
        Assert.Equal("save-current-named-playlist-to", saved.CommandResult?.Command);
        Assert.Equal(definitionId, saved.CommandResult?.TargetId);
        Assert.True(saved.CommandResult?.Succeeded);
    }

    [Fact]
    public async Task Save_to_opaque_id_does_not_reuse_a_stale_success_after_validation_rejection()
    {
        var persistence = new OperatorNamedPlaylistPersistence([]);
        var controller = CreateController(persistence);
        var listed = await controller.RefreshNamedPlaylistsAsync();
        var definitionId = Assert.Single(
            listed.NamedPlaylist!.Definitions).DefinitionId;
        var first = await controller.SaveCurrentNamedPlaylistToAsync(definitionId);
        Assert.True(first.CommandResult?.Succeeded);
        Assert.Equal(1, persistence.ReplaceCalls);

        var action = LegacyFixedActionCatalog.Default
            .GetActions(LegacyFixedMarket.Overseas)
            .First(candidate => candidate.Available);
        for (var index = 0;
             index <= LegacyNamedPlaylistWorkflowController.MaximumItems;
             index++)
        {
            controller.ActivateFixedAction(action.Id);
        }

        var rejected = await controller.SaveCurrentNamedPlaylistToAsync(definitionId);

        Assert.Equal(1, persistence.ReplaceCalls);
        Assert.Equal(
            LegacyNamedPlaylistMutationOutcome.CommittedFresh,
            rejected.NamedPlaylist?.LastMutationOutcome);
        Assert.Equal(LegacyOperatorStatusKind.Warning, rejected.StatusKind);
        Assert.Equal("save-current-named-playlist-to", rejected.CommandResult?.Command);
        Assert.Equal(definitionId, rejected.CommandResult?.TargetId);
        Assert.False(rejected.CommandResult?.Succeeded);
    }

    [Theory]
    [InlineData("refresh-named-playlists")]
    [InlineData("select-named-playlist")]
    [InlineData("create-named-playlist")]
    [InlineData("delete-selected-named-playlist")]
    [InlineData("load-named-playlist-by-id")]
    [InlineData("save-current-named-playlist-to")]
    public void Native_dispatch_failure_emits_exact_terminal_receipt(
        string command)
    {
        var controller = CreateController(
            new OperatorNamedPlaylistPersistence([]));
        const string definitionId = "named-definition-opaque";

        var failed = controller.ReportNamedPlaylistCommandFailure(
            command,
            definitionId,
            "The native request was rejected before completion.");

        Assert.Equal(command, failed.CommandResult?.Command);
        Assert.Equal(definitionId, failed.CommandResult?.TargetId);
        Assert.False(failed.CommandResult?.Succeeded);
        Assert.Equal(LegacyOperatorStatusKind.Error, failed.StatusKind);
    }

    [Fact]
    public void Native_dispatch_failure_rejects_unrelated_command_names()
    {
        var controller = CreateController(
            new OperatorNamedPlaylistPersistence([]));

        Assert.Throws<ArgumentOutOfRangeException>(() =>
            controller.ReportNamedPlaylistCommandFailure(
                "load-selected-named-playlist",
                "named-definition-opaque",
                "Rejected."));
    }

    [Theory]
    [InlineData("refresh-named-playlists")]
    [InlineData("select-named-playlist")]
    [InlineData("create-named-playlist")]
    [InlineData("delete-selected-named-playlist")]
    public void Busy_cycle_completion_echoes_exact_dispatch_id(string command)
    {
        var controller = CreateController(
            new OperatorNamedPlaylistPersistence([]));

        var completed = controller.CompleteNamedPlaylistDispatch(
            command,
            "named-ui-42",
            succeeded: true);

        Assert.Equal(command, completed.CommandResult?.Command);
        Assert.Equal("named-ui-42", completed.CommandResult?.TargetId);
        Assert.True(completed.CommandResult?.Succeeded);
    }

    [Fact]
    public async Task Ambiguous_native_save_failure_latches_no_retry_quarantine()
    {
        var persistence = new OperatorNamedPlaylistPersistence([]);
        var controller = CreateController(persistence);
        const string definitionId = "named-definition-opaque";

        var failed = controller.ReportNamedPlaylistWriteOutcomeUnknown(
            "save-current-named-playlist-to",
            definitionId,
            "The accepted save ended without a definitive database outcome.");

        Assert.Equal("save-current-named-playlist-to", failed.CommandResult?.Command);
        Assert.Equal(definitionId, failed.CommandResult?.TargetId);
        Assert.False(failed.CommandResult?.Succeeded);
        Assert.True(failed.NamedPlaylist?.IsWriteQuarantined);
        Assert.Equal(
            LegacyNamedPlaylistMutationOutcome.OutcomeUnknown,
            failed.NamedPlaylist?.LastMutationOutcome);

        var listed = await controller.RefreshNamedPlaylistsAsync();
        controller.SelectNamedPlaylist(
            Assert.Single(listed.NamedPlaylist!.Definitions).DefinitionId);
        var blocked = await controller.SaveCurrentNamedPlaylistAsync();

        Assert.Equal(0, persistence.ReplaceCalls);
        Assert.Equal(
            LegacyNamedPlaylistMutationOutcome.Quarantined,
            blocked.NamedPlaylist?.LastMutationOutcome);
    }

    private static LegacyOperatorController CreateController(
        INamedPlaylistPersistenceService persistence,
        TimeProvider? timeProvider = null,
        ILegacyScenePagePlanProvider? fixedPagePlanProvider = null) =>
        new(
            new UnusedLookup(),
            timeProvider: timeProvider,
            namedPlaylistWorkflow: new LegacyNamedPlaylistWorkflowController(persistence),
            fixedPagePlanProvider: fixedPagePlanProvider);

    private sealed class FixedSixRowPagePlanProvider : ILegacyScenePagePlanProvider
    {
        public Task<LegacyScenePagePlan> CreatePagePlanAsync(
            LegacyPlaylistEntry entry,
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            Assert.Equal("5077", entry.CutCode);
            return Task.FromResult(new LegacyScenePagePlan(
                "s5077",
                17,
                ScenePageSize.Six,
                3,
                17,
                false));
        }
    }

    private sealed class UnusedLookup : ILegacyStockLookup
    {
        public Task<LegacyStockSearchData> SearchAsync(
            string query,
            CancellationToken cancellationToken = default) =>
            Task.FromResult(new LegacyStockSearchData(query, [], []));
    }

    private sealed class FixedTimeProvider(DateTimeOffset localNow) : TimeProvider
    {
        private static readonly TimeZoneInfo KoreaStandardTime = TimeZoneInfo.CreateCustomTimeZone(
            "NamedThemeFixedKst",
            TimeSpan.FromHours(9),
            "Named theme fixed KST",
            "Named theme fixed KST");

        public override DateTimeOffset GetUtcNow() => localNow.ToUniversalTime();

        public override TimeZoneInfo LocalTimeZone => KoreaStandardTime;
    }

    private sealed class ExactThemeService : IThemeSelectionService
    {
        private readonly ThemeSelectionIdentity _identity;

        public ExactThemeService(ThemeSelectionIdentity identity)
        {
            _identity = identity;
        }

        public Task<ThemeSearchResult> SearchAsync(
            string query,
            ThemeSession nxtSession,
            int maximumResults = LegacyThemeSelectionService.DefaultMaximumResults,
            CancellationToken cancellationToken = default)
        {
            var identity = _identity.Market == ThemeMarket.Nxt
                ? _identity with { Session = nxtSession }
                : _identity;
            return Task.FromResult(new ThemeSearchResult(
                query,
                nxtSession,
                DateTimeOffset.UtcNow,
                [new ThemeSelectorItem(
                    identity,
                    identity.Market == ThemeMarket.Krx
                        ? DataSourceKind.Oracle
                        : DataSourceKind.MariaDb)],
                false));
        }

        public Task<ThemePreviewResult> PreviewAsync(
            ThemeSelectionIdentity identity,
            int maximumItems = LegacyThemeSelectionService.DefaultMaximumPreviewItems,
            CancellationToken cancellationToken = default) =>
            Task.FromResult(new ThemePreviewResult(
                identity,
                DateTimeOffset.UtcNow,
                [new ThemeItemPreview(
                    0,
                    identity.Market == ThemeMarket.Krx ? "P005930" : "X005930",
                    "Samsung Electronics")],
                false));
    }

    private sealed class OperatorNamedPlaylistPersistence : INamedPlaylistPersistenceService
    {
        private const string ProgramCode = "00000001";
        private const string Title = "운영 재생목록";
        private List<NamedPlaylistStoredItem> _items;
        private bool _exists = true;

        public OperatorNamedPlaylistPersistence(IEnumerable<NamedPlaylistStoredItem> items)
        {
            _items = items.Select(Clone).ToList();
        }

        public bool CanMutate => true;

        public int ListCalls { get; private set; }

        public int LoadCalls { get; private set; }

        public int ReplaceCalls { get; private set; }

        public int CreateCalls { get; private set; }

        public int DeleteCalls { get; private set; }

        public IReadOnlyList<NamedPlaylistStoredItem>? LastReplaceItems { get; private set; }

        public bool FailNextLoad { get; set; }

        public Task<NamedPlaylistListResult> ListAsync(
            int maximumResults = LegacyNamedPlaylistPersistenceService.DefaultMaximumDefinitions,
            CancellationToken cancellationToken = default)
        {
            ++ListCalls;
            IReadOnlyList<NamedPlaylistSummary> summaries = _exists
                ? [new NamedPlaylistSummary(ProgramCode, Title)]
                : [];
            return Task.FromResult(new NamedPlaylistListResult(
                DateTimeOffset.UtcNow,
                summaries,
                false));
        }

        public Task<string> SuggestNextProgramCodeAsync(
            CancellationToken cancellationToken = default) =>
            Task.FromResult(_exists ? "00000002" : ProgramCode);

        public Task<NamedPlaylistDocument> LoadAsync(
            string programCode,
            CancellationToken cancellationToken = default)
        {
            ++LoadCalls;
            if (FailNextLoad)
            {
                FailNextLoad = false;
                return Task.FromException<NamedPlaylistDocument>(
                    new NamedPlaylistDataException("The committed save could not be read back."));
            }

            if (!_exists || !string.Equals(programCode, ProgramCode, StringComparison.Ordinal))
            {
                return Task.FromException<NamedPlaylistDocument>(
                    new NamedPlaylistNotFoundException(programCode));
            }

            return Task.FromResult(new NamedPlaylistDocument(
                new NamedPlaylistSummary(ProgramCode, Title),
                DateTimeOffset.UtcNow,
                _items.Select(Clone).ToArray()));
        }

        public Task CreateDefinitionAsync(
            string programCode,
            string title,
            CancellationToken cancellationToken = default)
        {
            ++CreateCalls;
            _exists = true;
            _items = [];
            return Task.CompletedTask;
        }

        public Task ReplaceItemsAsync(
            string programCode,
            IReadOnlyList<NamedPlaylistStoredItem> items,
            CancellationToken cancellationToken = default)
        {
            ++ReplaceCalls;
            LastReplaceItems = items.Select(Clone).ToArray();
            _items = items.Select(Clone).ToList();
            return Task.CompletedTask;
        }

        public Task DeleteAsync(
            string programCode,
            CancellationToken cancellationToken = default)
        {
            ++DeleteCalls;
            _exists = false;
            _items = [];
            return Task.CompletedTask;
        }

        private static NamedPlaylistStoredItem Clone(NamedPlaylistStoredItem item) =>
            new(
                item.ItemIndex,
                item.IsEnabled,
                item.Selection with { },
                item.Page is null
                    ? null
                    : new NamedPlaylistPageState(
                        item.Page.CurrentPage,
                        item.Page.TotalPages));
    }
}
