using System.Data;
using MMoneyCoderSharp.Data;

namespace MBN_STOCK_WEBVIEW.LegacyApplication.Tests;

public sealed class LegacyManualFinancialWorkflowTests
{
    [Fact]
    public async Task GraphE_stock_picker_uses_the_complete_finite_core_boundary()
    {
        var service = new FakeManualFinancialService();
        var stockSearch = new FakeStockSearchService();
        var workflow = new LegacyManualFinancialWorkflow(
            service,
            stockSearch,
            new LegacyManualFinancialWriteSafetyState());

        await workflow.SearchStocksAsync(
            workflow.CreateInitial(ManualFinancialScreenKind.Sales),
            "Alpha");

        Assert.Equal(
            LegacyStockSearchService.MaximumResults,
            LegacyManualFinancialWorkflow.MaximumStockResults);
        Assert.Equal(
            LegacyStockSearchService.MaximumResults,
            stockSearch.LastMaximumResults);
    }

    [Fact]
    public async Task Empty_GraphE_stock_search_is_the_original_quiet_no_op()
    {
        var stockSearch = new FakeStockSearchService(
            new StockSearchItem(
                StockMarket.Kospi,
                DataSourceKind.Oracle,
                "Alpha",
                "000001"));
        var workflow = new LegacyManualFinancialWorkflow(
            new FakeManualFinancialService(),
            stockSearch,
            new LegacyManualFinancialWriteSafetyState());
        var initial = workflow.CreateInitial(ManualFinancialScreenKind.Sales);

        var unchanged = await workflow.SearchStocksAsync(initial, "   ");

        Assert.Same(initial, unchanged);
        Assert.Equal(0, stockSearch.LastMaximumResults);
        Assert.Empty(unchanged.StockCandidates);
        Assert.Null(unchanged.LastMessage);

        Assert.Same(unchanged, workflow.SelectStock(unchanged, string.Empty));
    }

    public static TheoryData<
        ManualFinancialScreenKind,
        string,
        string,
        string,
        string,
        string,
        string> Screens => new()
    {
        {
            ManualFinancialScreenKind.RevenueComposition,
            "revenue-composition",
            "주요매출 구성",
            "INPUT_PIE",
            "s5076",
            "5076",
            "REVENUE_COMPOSITION"
        },
        {
            ManualFinancialScreenKind.GrowthMetrics,
            "growth-metrics",
            "성장성 지표",
            "INPUT_GROW",
            "s5079",
            "5079",
            "GROWTH_METRICS"
        },
        {
            ManualFinancialScreenKind.Sales,
            "sales",
            "매출액",
            "INPUT_SELL",
            "s5080",
            "5080",
            "SALES"
        },
        {
            ManualFinancialScreenKind.OperatingProfit,
            "operating-profit",
            "영업이익",
            "INPUT_PROFIT",
            "s5081",
            "5081",
            "OPERATING_PROFIT"
        }
    };

    [Theory]
    [MemberData(nameof(Screens))]
    public void Registry_matches_every_GraphE_table_and_cut_contract(
        ManualFinancialScreenKind screen,
        string actionId,
        string label,
        string table,
        string builder,
        string code,
        string graphic)
    {
        var definition = LegacyManualFinancialWorkflow.GetScreen(screen);

        Assert.Equal(4, LegacyManualFinancialWorkflow.Screens.Count);
        Assert.Equal(actionId, definition.ActionId);
        Assert.Equal(label, definition.Label);
        Assert.Equal(table, definition.TableName);
        Assert.Equal(builder, definition.BuilderKey);
        Assert.Equal(code, definition.Code);
        Assert.Equal([code], definition.Aliases);
        Assert.Equal(graphic, definition.CanonicalGraphicType);
        Assert.Equal("DELETE_ALL_" + table, definition.DeleteAllConfirmationToken);
    }

    [Fact]
    public async Task Search_then_load_uses_opaque_identity_and_fresh_exact_read()
    {
        var original = Snapshot(Record(ManualFinancialScreenKind.Sales, "Alpha"), 1);
        var refreshed = Snapshot(Record(ManualFinancialScreenKind.Sales, "Alpha", 100), 2);
        var service = new FakeManualFinancialService(original)
        {
            GetOverride = _ => refreshed
        };
        var workflow = Workflow(service);
        var state = workflow.CreateInitial(ManualFinancialScreenKind.Sales);

        state = await workflow.SearchAsync(state, " Alpha ");

        Assert.Equal("Alpha", state.Query);
        Assert.Equal(LegacyManualFinancialWorkflow.MaximumListResults, service.LastMaximumResults);
        var row = Assert.Single(state.Rows);
        Assert.StartsWith("mf-row-", row.ResultId, StringComparison.Ordinal);
        Assert.Equal(
            ["Alpha", "1Q_1", "2Q_2", "3Q_3", "4Q_4", "5Q_5", "6Q_6"],
            row.PreviewValues);
        Assert.Null(state.SelectedRecord);
        Assert.False(state.IsDetailFresh);

        state = await workflow.LoadAsync(state, row.ResultId);

        Assert.True(state.IsDetailFresh);
        Assert.Equal(Version(2), state.SelectedRowVersion);
        Assert.Same(state.SelectedRecord, state.SelectedRecord);
        Assert.Equal(1, service.GetCalls);
        Assert.Equal("Alpha", service.LastGetIdentity?.StockName);
        Assert.Equal(Version(2), Assert.Single(state.Rows).RowVersion);
    }

    [Fact]
    public async Task Search_surfaces_isolated_legacy_row_count_without_hiding_valid_rows()
    {
        var valid = Snapshot(Record(ManualFinancialScreenKind.Sales, "Alpha"), 1);
        var service = new FakeManualFinancialService(valid)
        {
            SearchOverride = (screen, query) => new ManualFinancialSearchResult(
                screen,
                query,
                DateTimeOffset.UtcNow,
                [valid],
                false)
            {
                RejectedItemCount = 3,
                UnreadableStockNames = ["Broken", "BadVersion", "Legacy"]
            }
        };
        var workflow = Workflow(service);

        var state = await workflow.SearchAsync(
            workflow.CreateInitial(ManualFinancialScreenKind.Sales));

        Assert.Equal(4, state.Rows.Count);
        Assert.Single(state.Rows, row => row.IsDataReadable);
        Assert.Equal(3, state.Rows.Count(row => !row.IsDataReadable));
        Assert.Equal(3, state.ListRejectedItemCount);
        Assert.Contains("3행", state.LastMessage, StringComparison.Ordinal);
    }

    [Fact]
    public async Task Raw_GraphE_row_can_edit_delete_and_schedule_while_playout_stays_unready()
    {
        var table = RawSalesTable("Broken", "1Q_not-a-number");
        var service = new LegacyManualFinancialScreenService(
            new StaticQueryExecutor(table),
            new NoCallMutationExecutor());
        var workflow = new LegacyManualFinancialWorkflow(
            service,
            new FakeStockSearchService(
                new StockSearchItem(
                    StockMarket.Kospi,
                    DataSourceKind.Oracle,
                    "Broken",
                    "000001")),
            new LegacyManualFinancialWriteSafetyState());
        var state = await workflow.SearchAsync(
            workflow.CreateInitial(ManualFinancialScreenKind.Sales));
        var row = Assert.Single(state.Rows);
        Assert.True(row.IsDataReadable);
        Assert.False(row.IsPlayoutReady);
        Assert.Equal(
            ["Broken", "1Q_not-a-number", "2Q_20", "3Q_30", "4Q_40", "5Q_50", "6Q_60"],
            row.PreviewValues);
        Assert.Throws<NotSupportedException>(() =>
            ((IList<string>)row.PreviewValues).Add("ROWID"));

        state = await workflow.LoadAsync(state, row.ResultId);
        Assert.True(state.IsRawDetailFresh);
        Assert.False(state.IsDetailFresh);
        Assert.False(state.IsPlayoutReady);
        Assert.NotNull(state.SelectedRawRecord);
        Assert.Null(state.SelectedRecord);

        state = await workflow.SearchStocksAsync(state, "Broken");
        state = workflow.SelectStock(state, Assert.Single(state.StockCandidates).ResultId);
        Assert.True(state.CanEdit);
        Assert.True(state.CanSave);
        Assert.True(state.CanDelete);
        Assert.True(state.CanMaterializePlaylist);

        var replacement = new ManualFinancialRawRecord(
            state.SelectedRawRecord!.Identity,
            state.SelectedRawRecord.StorageValues.ToArray());
        Assert.IsType<LegacyManualFinancialRawUpdateRequest>(
            workflow.CreateRawUpdateRequest(state, replacement));
        Assert.IsType<LegacyManualFinancialRawDeleteRequest>(
            workflow.CreateDeleteRequest(state));
        var draft = workflow.MaterializePlaylistDraft(state, "sales");
        Assert.Null(draft.OperatorSnapshot);
        Assert.NotNull(draft.OperatorRawSnapshot);
        Assert.Equal("Broken", draft.CutSelection.Selection.Subject);
    }

    [Fact]
    public async Task GraphE_name_header_and_directional_find_are_CSharp_authoritative()
    {
        var service = new FakeManualFinancialService(
            Snapshot(Record(ManualFinancialScreenKind.Sales, "Gamma"), 1),
            Snapshot(Record(ManualFinancialScreenKind.Sales, "Alpha"), 2),
            Snapshot(Record(ManualFinancialScreenKind.Sales, "Beta"), 3));
        var workflow = Workflow(service);
        var state = await workflow.SearchAsync(
            workflow.CreateInitial(ManualFinancialScreenKind.Sales));

        Assert.Equal(["Alpha", "Beta", "Gamma"], state.Rows.Select(row => row.StockName));
        Assert.Equal(LegacyManualFinancialNameSortDirection.Ascending,
            state.NameSortDirection);

        state = workflow.FindByName(
            state,
            "a",
            LegacyManualFinancialFindDirection.Down,
            state.Revision);
        Assert.Equal("Alpha", state.Rows.Single(row =>
            row.ResultId == state.SelectedRowId).StockName);
        state = workflow.FindByName(
            state,
            "a",
            LegacyManualFinancialFindDirection.Down,
            state.Revision);
        Assert.Equal("Beta", state.Rows.Single(row =>
            row.ResultId == state.SelectedRowId).StockName);
        state = workflow.FindByName(
            state,
            "a",
            LegacyManualFinancialFindDirection.Up,
            state.Revision);
        Assert.Equal("Alpha", state.Rows.Single(row =>
            row.ResultId == state.SelectedRowId).StockName);

        var staleGeneration = state.Revision;
        state = workflow.ToggleNameSort(state, staleGeneration);
        Assert.Equal(["Gamma", "Beta", "Alpha"], state.Rows.Select(row => row.StockName));
        Assert.Equal(LegacyManualFinancialNameSortDirection.Descending,
            state.NameSortDirection);
        Assert.Throws<InvalidOperationException>(() =>
            workflow.ToggleNameSort(state, staleGeneration));
    }

    [Fact]
    public async Task Directional_find_matches_Hangul_initial_consonants()
    {
        var service = new FakeManualFinancialService(
            Snapshot(Record(ManualFinancialScreenKind.Sales, "가나다"), 1),
            Snapshot(Record(ManualFinancialScreenKind.Sales, "삼성전자"), 2),
            Snapshot(Record(ManualFinancialScreenKind.Sales, "현대차"), 3));
        var workflow = Workflow(service);
        var state = await workflow.SearchAsync(
            workflow.CreateInitial(ManualFinancialScreenKind.Sales));

        state = workflow.FindByName(
            state,
            " ㅅㅅㅈ ",
            LegacyManualFinancialFindDirection.Down,
            state.Revision);

        Assert.Equal("ㅅㅅㅈ", state.FindQuery);
        Assert.Equal(
            "삼성전자",
            state.Rows.Single(row => row.ResultId == state.SelectedRowId).StockName);

        state = workflow.FindByName(
            state,
            "ㄱㄴㄷ",
            LegacyManualFinancialFindDirection.Up,
            state.Revision);

        Assert.Equal(
            "가나다",
            state.Rows.Single(row => row.ResultId == state.SelectedRowId).StockName);
    }

    [Fact]
    public async Task Create_keeps_a_matching_row_inside_an_initial_consonant_filter()
    {
        const string stockName = "삼성전자";
        var service = new FakeManualFinancialService
        {
            SearchOverride = (screen, query) => new ManualFinancialSearchResult(
                screen,
                query,
                DateTimeOffset.UtcNow,
                Array.Empty<ManualFinancialSnapshot>(),
                IsTruncated: false)
        };
        var workflow = new LegacyManualFinancialWorkflow(
            service,
            new FakeStockSearchService(new StockSearchItem(
                StockMarket.Kospi,
                DataSourceKind.Oracle,
                stockName,
                "005930")),
            new LegacyManualFinancialWriteSafetyState());
        var state = await workflow.SearchAsync(
            workflow.CreateInitial(ManualFinancialScreenKind.Sales),
            "ㅅㅅㅈ");
        state = await workflow.SearchStocksAsync(state, stockName);
        state = workflow.SelectStock(
            state,
            Assert.Single(state.StockCandidates).ResultId);

        state = await workflow.ExecuteAsync(
            state,
            workflow.CreateCreateRequest(
                state,
                Record(ManualFinancialScreenKind.Sales, stockName)));

        Assert.Equal("ㅅㅅㅈ", state.Query);
        Assert.Equal(stockName, Assert.Single(state.Rows).StockName);
    }

    [Theory]
    [MemberData(nameof(Screens))]
    public async Task Create_save_readback_and_action_id_only_draft_work_for_all_modes(
        ManualFinancialScreenKind screen,
        string actionId,
        string label,
        string table,
        string builder,
        string code,
        string graphic)
    {
        _ = table;
        var service = new FakeManualFinancialService();
        var workflow = Workflow(service);
        var state = workflow.CreateInitial(screen);
        state = await VerifyStockAsync(workflow, state, "Alpha");
        var request = workflow.CreateCreateRequest(state, Record(screen, "Alpha"));

        Assert.Equal(ManualFinancialMutationKind.Create, request.Kind);
        Assert.Equal(screen, request.Screen);
        Assert.Equal("Alpha", request.StockName);
        Assert.Matches("^[a-f0-9]{32}$", request.RequestId);

        state = await workflow.ExecuteAsync(state, request);
        var draft = workflow.MaterializePlaylistDraft(state, actionId);

        Assert.Equal(LegacyManualFinancialWorkflowStatus.WriteCommitted, state.Status);
        Assert.True(state.IsDetailFresh);
        Assert.Equal(1, service.MutationCalls);
        Assert.Equal(1, service.GetCalls);
        Assert.Equal(request.RequestId, state.LastRequestId);
        Assert.Equal(builder, draft.BuilderKey);
        Assert.Equal(code, draft.Code);
        Assert.Equal([code], draft.Aliases);
        Assert.Equal(label, draft.Detail);
        Assert.Equal("manual-financial", draft.Category);
        Assert.Equal(graphic, draft.CutSelection.Selection.GraphicType);
        Assert.Equal(builder, draft.CutSelection.BuilderKey);
        Assert.Equal(code, draft.CutSelection.CutCode);
        Assert.Equal(1, draft.CutSelection.CurrentPage);
        Assert.Equal(1, draft.CutSelection.TotalPages);
        Assert.Equal(state.SelectedRowVersion, draft.OperatorSnapshot!.RowVersion);
        Assert.Equal("KOSPI", draft.Market);
        Assert.Equal("000001", draft.StockCode);
        Assert.False(draft.IsNxt);
    }

    [Fact]
    public async Task Update_request_carries_selected_rowVersion_and_rebinds_fresh_version()
    {
        var service = new FakeManualFinancialService(
            Snapshot(Record(ManualFinancialScreenKind.GrowthMetrics, "Alpha"), 1));
        var workflow = Workflow(service);
        var state = await LoadAndVerifyAsync(
            workflow,
            ManualFinancialScreenKind.GrowthMetrics,
            "Alpha");
        var replacement = Record(ManualFinancialScreenKind.GrowthMetrics, "Alpha", 100);
        var request = workflow.CreateUpdateRequest(state, replacement);

        Assert.Equal(Version(1), request.ExpectedRowVersion);
        state = await workflow.ExecuteAsync(state, request);

        Assert.Equal(Version(11), state.SelectedRowVersion);
        Assert.Equal(Version(1), service.LastExpected?.RowVersion);
        Assert.Same(replacement.Identity, replacement.Identity);
        Assert.NotSame(replacement, request.Replacement);
        var draft = workflow.MaterializePlaylistDraft(state, "growth-metrics");
        Assert.Equal(Version(11), draft.OperatorSnapshot!.RowVersion);
        Assert.Equal(1, service.MutationCalls);
        Assert.Equal(2, service.GetCalls);
    }

    [Fact]
    public async Task Delete_one_uses_optimistic_snapshot_and_clears_selection()
    {
        var service = new FakeManualFinancialService(
            Snapshot(Record(ManualFinancialScreenKind.OperatingProfit, "Alpha"), 1));
        var workflow = Workflow(service);
        var state = workflow.CreateInitial(ManualFinancialScreenKind.OperatingProfit);
        state = await workflow.SearchAsync(state);
        state = await workflow.LoadAsync(state, Assert.Single(state.Rows).ResultId);
        var request = workflow.CreateDeleteRequest(state);

        Assert.Equal(Version(1), request.ExpectedRowVersion);
        state = await workflow.ExecuteAsync(state, request);

        Assert.Empty(state.Rows);
        Assert.Null(state.SelectedRecord);
        Assert.False(state.IsDetailFresh);
        Assert.Equal(Version(1), service.LastExpected?.RowVersion);
        Assert.Equal(ManualFinancialMutationKind.DeleteOne, state.LastReceipt?.Kind);
    }

    [Fact]
    public async Task Delete_all_requires_the_fixed_screen_token_and_accepts_zero_rows()
    {
        var service = new FakeManualFinancialService(
            Snapshot(Record(ManualFinancialScreenKind.RevenueComposition, "Alpha"), 1));
        var workflow = Workflow(service);
        var state = workflow.CreateInitial(ManualFinancialScreenKind.RevenueComposition);
        state = await workflow.SearchAsync(state);

        Assert.Throws<ArgumentException>(() =>
            workflow.CreateDeleteAllRequest(state, "DELETE_ALL_INPUT_SELL"));
        var request = workflow.CreateDeleteAllRequest(
            state,
            state.Definition.DeleteAllConfirmationToken);
        service.DeleteAllAffectedRows = 0;
        state = await workflow.ExecuteAsync(state, request);

        Assert.Equal("DELETE_ALL_INPUT_PIE", request.ConfirmationToken);
        Assert.Empty(state.Rows);
        Assert.Equal(0, state.LastReceipt?.AffectedRows);
        Assert.Equal(1, service.MutationCalls);
    }

    [Fact]
    public async Task Exact_stock_resolution_rejects_duplicate_master_names()
    {
        var service = new FakeManualFinancialService();
        var stockSearch = new FakeStockSearchService(
            new(StockMarket.Kospi, DataSourceKind.Oracle, "Alpha", "000001"),
            new(StockMarket.Kosdaq, DataSourceKind.Oracle, "Alpha", "000002"));
        var workflow = new LegacyManualFinancialWorkflow(
            service,
            stockSearch,
            new LegacyManualFinancialWriteSafetyState());
        var state = workflow.CreateInitial(ManualFinancialScreenKind.Sales);
        state = await workflow.SearchStocksAsync(state, "Alpha");

        Assert.Throws<ArgumentException>(() =>
            workflow.SelectStock(state, state.StockCandidates[0].ResultId));
    }

    [Fact]
    public async Task Update_and_playlist_require_detail_bound_stock_verification()
    {
        var service = new FakeManualFinancialService(
            Snapshot(Record(ManualFinancialScreenKind.Sales, "Alpha"), 1));
        var workflow = Workflow(service);
        var state = workflow.CreateInitial(ManualFinancialScreenKind.Sales);
        state = await workflow.SearchAsync(state);
        state = await workflow.LoadAsync(state, Assert.Single(state.Rows).ResultId);

        Assert.Throws<InvalidOperationException>(() =>
            workflow.CreateUpdateRequest(state, Record(ManualFinancialScreenKind.Sales, "Alpha")));
        Assert.Throws<InvalidOperationException>(() =>
            workflow.MaterializePlaylistDraft(state, "sales"));

        state = await VerifyStockAsync(workflow, state, "Alpha");
        Assert.NotNull(workflow.CreateUpdateRequest(
            state,
            Record(ManualFinancialScreenKind.Sales, "Alpha", 10)));
        Assert.NotNull(workflow.MaterializePlaylistDraft(state, "sales"));
    }

    [Fact]
    public async Task OutcomeUnknown_latches_process_write_quarantine_and_never_retries()
    {
        var service = new FakeManualFinancialService
        {
            MutationException = new ManualFinancialMutationException("timeout", outcomeUnknown: true)
        };
        var workflow = Workflow(service);
        var state = workflow.CreateInitial(ManualFinancialScreenKind.Sales);
        state = await VerifyStockAsync(workflow, state, "Alpha");
        var request = workflow.CreateCreateRequest(
            state,
            Record(ManualFinancialScreenKind.Sales, "Alpha"));
        var original = state;

        state = await workflow.ExecuteAsync(state, request);

        Assert.Equal(LegacyManualFinancialWorkflowStatus.OutcomeUnknown, state.Status);
        Assert.True(state.WritesQuarantined);
        Assert.Equal(1, service.MutationCalls);
        Assert.Throws<InvalidOperationException>(() =>
            workflow.CreateCreateRequest(
                original,
                Record(ManualFinancialScreenKind.Sales, "Alpha")));
        await Assert.ThrowsAsync<InvalidOperationException>(() =>
            workflow.ExecuteAsync(original, request));
        Assert.Equal(1, service.MutationCalls);
        Assert.True(workflow.CreateInitial(ManualFinancialScreenKind.Sales).WritesQuarantined);
        Assert.Throws<InvalidOperationException>(() =>
            workflow.MaterializePlaylistDraft(state, "sales"));
    }

    [Fact]
    public async Task Malformed_write_receipt_is_quarantined_as_unknown()
    {
        var service = new FakeManualFinancialService
        {
            ReceiptOverride = (kind, screen, name, affectedRows) => new(
                Guid.NewGuid(),
                ManualFinancialMutationKind.DeleteOne,
                screen,
                name,
                DateTimeOffset.UtcNow,
                affectedRows)
        };
        var workflow = Workflow(service);
        var state = workflow.CreateInitial(ManualFinancialScreenKind.Sales);
        state = await VerifyStockAsync(workflow, state, "Alpha");
        var request = workflow.CreateCreateRequest(
            state,
            Record(ManualFinancialScreenKind.Sales, "Alpha"));

        state = await workflow.ExecuteAsync(state, request);

        Assert.Equal(LegacyManualFinancialWorkflowStatus.OutcomeUnknown, state.Status);
        Assert.True(state.WritesQuarantined);
        Assert.Equal(1, service.MutationCalls);
        Assert.Equal(0, service.GetCalls);
    }

    [Fact]
    public async Task Known_rollback_is_not_quarantined_but_request_stays_one_shot()
    {
        var service = new FakeManualFinancialService
        {
            MutationException = new ManualFinancialMutationRejectedException(
                ManualFinancialMutationRejectionReason.DuplicateIdentity,
                "rolled back")
        };
        var workflow = Workflow(service);
        var state = workflow.CreateInitial(ManualFinancialScreenKind.Sales);
        state = await VerifyStockAsync(workflow, state, "Alpha");
        var request = workflow.CreateCreateRequest(
            state,
            Record(ManualFinancialScreenKind.Sales, "Alpha"));
        var source = state;

        state = await workflow.ExecuteAsync(state, request);

        Assert.Equal(LegacyManualFinancialWorkflowStatus.WriteRejected, state.Status);
        Assert.False(state.WritesQuarantined);
        await Assert.ThrowsAsync<InvalidOperationException>(() =>
            workflow.ExecuteAsync(source, request));
        Assert.Equal(1, service.MutationCalls);
    }

    [Fact]
    public async Task Sibling_requests_from_one_snapshot_cannot_execute_twice()
    {
        var service = new FakeManualFinancialService();
        var workflow = Workflow(service);
        var state = workflow.CreateInitial(ManualFinancialScreenKind.Sales);
        state = await VerifyStockAsync(workflow, state, "Alpha");
        var first = workflow.CreateCreateRequest(
            state,
            Record(ManualFinancialScreenKind.Sales, "Alpha"));
        var sibling = workflow.CreateCreateRequest(
            state,
            Record(ManualFinancialScreenKind.Sales, "Alpha"));

        _ = await workflow.ExecuteAsync(state, first);

        await Assert.ThrowsAsync<InvalidOperationException>(() =>
            workflow.ExecuteAsync(state, sibling));
        Assert.Equal(1, service.MutationCalls);
    }

    [Fact]
    public async Task Pre_write_fresh_snapshot_cannot_materialize_after_update()
    {
        var service = new FakeManualFinancialService(
            Snapshot(Record(ManualFinancialScreenKind.Sales, "Alpha"), 1));
        var workflow = Workflow(service);
        var stale = await LoadAndVerifyAsync(
            workflow,
            ManualFinancialScreenKind.Sales,
            "Alpha");
        var request = workflow.CreateUpdateRequest(
            stale,
            Record(ManualFinancialScreenKind.Sales, "Alpha", 100));

        var current = await workflow.ExecuteAsync(stale, request);

        Assert.Throws<InvalidOperationException>(() =>
            workflow.MaterializePlaylistDraft(stale, "sales"));
        Assert.NotNull(workflow.MaterializePlaylistDraft(current, "sales"));
    }

    [Fact]
    public async Task Shared_process_safety_quarantines_new_workflow_instances()
    {
        var safety = new LegacyManualFinancialWriteSafetyState();
        var firstService = new FakeManualFinancialService
        {
            MutationException = new ManualFinancialMutationException("timeout", outcomeUnknown: true)
        };
        var first = Workflow(firstService, safety);
        var state = first.CreateInitial(ManualFinancialScreenKind.Sales);
        state = await VerifyStockAsync(first, state, "Alpha");
        state = await first.ExecuteAsync(
            state,
            first.CreateCreateRequest(
                state,
                Record(ManualFinancialScreenKind.Sales, "Alpha")));
        Assert.True(state.WritesQuarantined);

        var second = Workflow(new FakeManualFinancialService(), safety);
        var secondState = second.CreateInitial(ManualFinancialScreenKind.OperatingProfit);

        Assert.True(secondState.WritesQuarantined);
        Assert.Throws<InvalidOperationException>(() =>
            second.CreateDeleteAllRequest(
                secondState,
                secondState.Definition.DeleteAllConfirmationToken));
    }

    [Fact]
    public async Task Committed_write_without_readback_never_enables_action()
    {
        var service = new FakeManualFinancialService();
        var workflow = Workflow(service);
        var state = workflow.CreateInitial(ManualFinancialScreenKind.Sales);
        state = await VerifyStockAsync(workflow, state, "Alpha");
        var request = workflow.CreateCreateRequest(
            state,
            Record(ManualFinancialScreenKind.Sales, "Alpha"));
        service.GetException = new InvalidOperationException("read unavailable");

        state = await workflow.ExecuteAsync(state, request);

        Assert.Equal(LegacyManualFinancialWorkflowStatus.ReadBackRequired, state.Status);
        Assert.NotNull(state.LastReceipt);
        Assert.Null(state.SelectedRecord);
        Assert.False(state.WritesQuarantined);
        Assert.Throws<InvalidOperationException>(() =>
            workflow.MaterializePlaylistDraft(state, "sales"));
    }

    [Fact]
    public async Task Request_is_revision_bound_and_foreign_snapshots_fail_closed()
    {
        var firstService = new FakeManualFinancialService();
        var first = Workflow(firstService);
        var state = first.CreateInitial(ManualFinancialScreenKind.Sales);
        state = await VerifyStockAsync(first, state, "Alpha");
        var request = first.CreateCreateRequest(
            state,
            Record(ManualFinancialScreenKind.Sales, "Alpha"));
        var changed = first.BeginCreate(state);

        await Assert.ThrowsAsync<ArgumentException>(() =>
            first.ExecuteAsync(changed, request));
        Assert.Equal(0, firstService.MutationCalls);

        var second = Workflow(new FakeManualFinancialService());
        Assert.Throws<ArgumentException>(() => second.BeginCreate(state));
        await Assert.ThrowsAsync<ArgumentException>(() =>
            second.SearchAsync(state));
    }

    [Fact]
    public async Task Read_only_configuration_blocks_request_creation_before_executor()
    {
        var service = new FakeManualFinancialService { CanMutate = false };
        var workflow = Workflow(service);
        var state = workflow.CreateInitial(ManualFinancialScreenKind.Sales);
        state = await VerifyStockAsync(workflow, state, "Alpha");

        Assert.Throws<InvalidOperationException>(() =>
            workflow.CreateCreateRequest(
                state,
                Record(ManualFinancialScreenKind.Sales, "Alpha")));
        Assert.Equal(0, service.MutationCalls);
    }

    [Fact]
    public async Task Duplicate_list_names_remain_visible_but_invalid_unreadable_identity_is_rejected()
    {
        var duplicate = Snapshot(Record(ManualFinancialScreenKind.Sales, "Alpha"), 1);
        var service = new FakeManualFinancialService
        {
            SearchOverride = (_, query) => new ManualFinancialSearchResult(
                ManualFinancialScreenKind.Sales,
                query,
                DateTimeOffset.UtcNow,
                [duplicate, duplicate],
                false)
        };
        var workflow = Workflow(service);

        var duplicates = await workflow.SearchAsync(
            workflow.CreateInitial(ManualFinancialScreenKind.Sales));
        Assert.Equal(2, duplicates.Rows.Count);

        service.SearchOverride = (_, query) => new ManualFinancialSearchResult(
            ManualFinancialScreenKind.Sales,
            query,
            DateTimeOffset.UtcNow,
            [],
            false)
        {
            RejectedItemCount = 1,
            UnreadableStockNames = [" invalid "]
        };

        await Assert.ThrowsAsync<LegacyManualFinancialWorkflowDataException>(() =>
            workflow.SearchAsync(workflow.CreateInitial(ManualFinancialScreenKind.Sales)));
    }

    [Fact]
    public async Task DTOs_and_snapshots_clone_mutable_input_collections()
    {
        var quarters = Enumerable.Range(1, 6)
            .Select(index => new ManualQuarterValue($"{index}Q", index))
            .ToArray();
        var record = new ManualSalesRecord(
            new ManualFinancialIdentity(ManualFinancialScreenKind.Sales, "Alpha"),
            quarters);
        var service = new FakeManualFinancialService();
        var workflow = Workflow(service);
        var state = workflow.CreateInitial(ManualFinancialScreenKind.Sales);
        state = await VerifyStockAsync(workflow, state, "Alpha");
        var request = workflow.CreateCreateRequest(state, record);

        quarters[0] = new ManualQuarterValue("BAD", 999);

        var cloned = Assert.IsType<ManualSalesRecord>(request.Record);
        Assert.Equal("1Q", cloned.Quarters[0].Quarter);
        Assert.Throws<NotSupportedException>(() =>
            ((IList<ManualQuarterValue>)cloned.Quarters).Add(new("7Q", 7)));

        state = await workflow.ExecuteAsync(state, request);
        Assert.Throws<NotSupportedException>(() =>
            ((IList<LegacyManualFinancialListRow>)state.Rows).Clear());
        var selected = Assert.IsType<ManualSalesRecord>(state.SelectedRecord);
        Assert.Throws<NotSupportedException>(() =>
            ((IList<ManualQuarterValue>)selected.Quarters).Clear());
    }

    [Fact]
    public async Task Wrong_action_id_and_cross_screen_action_never_materialize()
    {
        var service = new FakeManualFinancialService();
        var workflow = Workflow(service);
        var state = workflow.CreateInitial(ManualFinancialScreenKind.Sales);
        state = await VerifyStockAsync(workflow, state, "Alpha");
        state = await workflow.ExecuteAsync(
            state,
            workflow.CreateCreateRequest(
                state,
                Record(ManualFinancialScreenKind.Sales, "Alpha")));

        Assert.Throws<ArgumentException>(() =>
            workflow.MaterializePlaylistDraft(state, "s5080"));
        Assert.Throws<ArgumentException>(() =>
            workflow.MaterializePlaylistDraft(state, "operating-profit"));
    }

    private static LegacyManualFinancialWorkflow Workflow(
        FakeManualFinancialService service,
        LegacyManualFinancialWriteSafetyState? writeSafety = null) =>
        new(
            service,
            new FakeStockSearchService(
                new StockSearchItem(
                    StockMarket.Kospi,
                    DataSourceKind.Oracle,
                    "Alpha",
                    "000001")),
            writeSafety ?? new LegacyManualFinancialWriteSafetyState());

    private static async Task<LegacyManualFinancialWorkflowSnapshot> VerifyStockAsync(
        LegacyManualFinancialWorkflow workflow,
        LegacyManualFinancialWorkflowSnapshot state,
        string stockName)
    {
        state = await workflow.SearchStocksAsync(state, stockName);
        return workflow.SelectStock(state, Assert.Single(state.StockCandidates).ResultId);
    }

    private static async Task<LegacyManualFinancialWorkflowSnapshot> LoadAndVerifyAsync(
        LegacyManualFinancialWorkflow workflow,
        ManualFinancialScreenKind screen,
        string stockName)
    {
        var state = workflow.CreateInitial(screen);
        state = await workflow.SearchAsync(state, stockName);
        state = await workflow.LoadAsync(state, Assert.Single(state.Rows).ResultId);
        return await VerifyStockAsync(workflow, state, stockName);
    }

    private static ManualFinancialSnapshot Snapshot(ManualFinancialRecord record, int version) =>
        new(record, Version(version));

    private static string Version(int value) => value.ToString("X64");

    private static ManualFinancialRecord Record(
        ManualFinancialScreenKind screen,
        string stockName,
        int offset = 0)
    {
        var identity = new ManualFinancialIdentity(screen, stockName);
        return screen switch
        {
            ManualFinancialScreenKind.RevenueComposition =>
                new ManualRevenueCompositionRecord(
                    identity,
                    "2026.07",
                    [
                        new ManualRevenueSlice("반도체", "25", 25),
                        new ManualRevenueSlice("모바일", "20", 20),
                        null,
                        null,
                        null
                    ]),
            ManualFinancialScreenKind.GrowthMetrics =>
                new ManualGrowthMetricsRecord(
                    identity,
                    new ManualGrowthSeriesValues(1 + offset, 2, 3, 4),
                    new ManualGrowthSeriesValues(5, 6, 7, 8),
                    new ManualGrowthSeriesValues(9, 10, 11, 12),
                    new ManualGrowthSeriesValues(13, 14, 15, 16),
                    new ManualGrowthPeriodLabels("1Q", "2Q", "3Q", "4Q")),
            ManualFinancialScreenKind.Sales =>
                new ManualSalesRecord(
                    identity,
                    Enumerable.Range(1, 6)
                        .Select(index => new ManualQuarterValue(
                            $"{index}Q",
                            offset + index))
                        .ToArray()),
            ManualFinancialScreenKind.OperatingProfit =>
                new ManualOperatingProfitRecord(
                    identity,
                    Enumerable.Range(1, 6)
                        .Select(index => new ManualQuarterValue(
                            $"{index}Q",
                            offset + index))
                        .ToArray()),
            _ => throw new ArgumentOutOfRangeException(nameof(screen))
        };
    }

    private static DataTable RawSalesTable(string stockName, string firstValue)
    {
        var table = new DataTable();
        foreach (var column in ManualFinancialCutContracts.Get(
                     ManualFinancialScreenKind.Sales).StorageColumns)
        {
            table.Columns.Add(column, typeof(string));
        }
        table.Columns.Add("ROW_VERSION", typeof(string));
        table.Columns.Add("ROW_HANDLE", typeof(string));
        table.Rows.Add(
            stockName,
            firstValue,
            "2Q_20",
            "3Q_30",
            "4Q_40",
            "5Q_50",
            "6Q_60",
            Version(1),
            "AAAAAAAAAAAAAAAAAA");
        return table;
    }

    private sealed class StaticQueryExecutor : IDataQueryExecutor
    {
        private readonly DataTable _table;

        internal StaticQueryExecutor(DataTable table) => _table = table;

        public DataTable Execute(DataSourceKind source, string tableName, string query) =>
            throw new NotSupportedException();

        public Task<DataTable> ExecuteAsync(
            DataSourceKind source,
            string tableName,
            string query,
            CancellationToken cancellationToken = default) =>
            throw new NotSupportedException();

        public Task<DataTable> ExecuteAsync(
            DataSourceKind source,
            string tableName,
            DataQuerySpec query,
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            return Task.FromResult(_table.Copy());
        }
    }

    private sealed class NoCallMutationExecutor : IManualFinancialMutationExecutor
    {
        public Task<ManualFinancialDbTransactionResult> ExecuteTransactionAsync(
            DataSourceKind source,
            ManualFinancialDbTransaction transaction,
            CancellationToken cancellationToken = default) =>
            throw new InvalidOperationException("The test must not issue a real mutation.");
    }

    private sealed class FakeStockSearchService : IStockSearchService
    {
        private readonly IReadOnlyList<StockSearchItem> _items;

        internal FakeStockSearchService(params StockSearchItem[] items)
        {
            _items = items;
        }

        internal int LastMaximumResults { get; private set; }

        public Task<StockSearchResult> SearchAsync(
            string query,
            int maximumResults = LegacyStockSearchService.DefaultMaximumResults,
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            LastMaximumResults = maximumResults;
            return Task.FromResult(new StockSearchResult(
                query,
                DateTimeOffset.UtcNow,
                _items.Take(maximumResults).ToArray(),
                _items.Count > maximumResults));
        }
    }

    private sealed class FakeManualFinancialService : IManualFinancialScreenService
    {
        private readonly Dictionary<(ManualFinancialScreenKind Screen, string Name),
            ManualFinancialSnapshot> _rows = new();
        private int _nextVersion = 10;

        internal FakeManualFinancialService(params ManualFinancialSnapshot[] rows)
        {
            foreach (var row in rows)
            {
                _rows[(row.Record.Identity.Screen, row.Record.Identity.StockName)] = row;
            }
        }

        public bool CanMutate { get; set; } = true;

        internal int MutationCalls { get; private set; }

        internal int GetCalls { get; private set; }

        internal int LastMaximumResults { get; private set; }

        internal ManualFinancialIdentity? LastGetIdentity { get; private set; }

        internal ManualFinancialSnapshot? LastExpected { get; private set; }

        internal Exception? MutationException { get; set; }

        internal Exception? GetException { get; set; }

        internal Func<ManualFinancialIdentity, ManualFinancialSnapshot>? GetOverride { get; set; }

        internal Func<ManualFinancialScreenKind, string, ManualFinancialSearchResult>?
            SearchOverride
        { get; set; }

        internal Func<
            ManualFinancialMutationKind,
            ManualFinancialScreenKind,
            string,
            int,
            ManualFinancialMutationReceipt>? ReceiptOverride
        { get; set; }

        internal int DeleteAllAffectedRows { get; set; }

        public Task<ManualFinancialSearchResult> SearchAsync(
            ManualFinancialScreenKind screen,
            string query = "",
            int maximumResults = LegacyManualFinancialScreenService.DefaultMaximumResults,
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            LastMaximumResults = maximumResults;
            if (SearchOverride is not null)
            {
                return Task.FromResult(SearchOverride(screen, query));
            }

            var all = _rows.Values
                .Where(row => row.Record.Identity.Screen == screen)
                .Where(row => query.Length == 0 || row.Record.Identity.StockName.Contains(
                    query,
                    StringComparison.OrdinalIgnoreCase))
                .OrderBy(row => row.Record.Identity.StockName, StringComparer.Ordinal)
                .ToArray();
            return Task.FromResult(new ManualFinancialSearchResult(
                screen,
                query,
                DateTimeOffset.UtcNow,
                all.Take(maximumResults).ToArray(),
                all.Length > maximumResults));
        }

        public Task<ManualFinancialSnapshot> GetAsync(
            ManualFinancialIdentity identity,
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            GetCalls++;
            LastGetIdentity = identity;
            if (GetException is not null)
            {
                throw GetException;
            }

            if (GetOverride is not null)
            {
                return Task.FromResult(GetOverride(identity));
            }

            return Task.FromResult(_rows[(identity.Screen, identity.StockName)]);
        }

        public Task<ManualFinancialMutationReceipt> CreateAsync(
            ManualFinancialRecord record,
            CancellationToken cancellationToken = default)
        {
            BeforeMutation(cancellationToken);
            var snapshot = new ManualFinancialSnapshot(record, Version(++_nextVersion));
            _rows[(record.Identity.Screen, record.Identity.StockName)] = snapshot;
            return Task.FromResult(Receipt(
                ManualFinancialMutationKind.Create,
                record.Identity.Screen,
                record.Identity.StockName,
                1));
        }

        public Task<ManualFinancialMutationReceipt> UpdateAsync(
            ManualFinancialSnapshot expected,
            ManualFinancialRecord replacement,
            CancellationToken cancellationToken = default)
        {
            BeforeMutation(cancellationToken);
            LastExpected = expected;
            _rows[(replacement.Identity.Screen, replacement.Identity.StockName)] =
                new ManualFinancialSnapshot(replacement, Version(++_nextVersion));
            return Task.FromResult(Receipt(
                ManualFinancialMutationKind.Update,
                replacement.Identity.Screen,
                replacement.Identity.StockName,
                1));
        }

        public Task<ManualFinancialMutationReceipt> DeleteAsync(
            ManualFinancialSnapshot expected,
            CancellationToken cancellationToken = default)
        {
            BeforeMutation(cancellationToken);
            LastExpected = expected;
            _rows.Remove((
                expected.Record.Identity.Screen,
                expected.Record.Identity.StockName));
            return Task.FromResult(Receipt(
                ManualFinancialMutationKind.DeleteOne,
                expected.Record.Identity.Screen,
                expected.Record.Identity.StockName,
                1));
        }

        public Task<ManualFinancialMutationReceipt> DeleteAllAsync(
            ManualFinancialScreenKind screen,
            CancellationToken cancellationToken = default)
        {
            BeforeMutation(cancellationToken);
            foreach (var key in _rows.Keys.Where(key => key.Screen == screen).ToArray())
            {
                _rows.Remove(key);
            }

            return Task.FromResult(Receipt(
                ManualFinancialMutationKind.DeleteAll,
                screen,
                string.Empty,
                DeleteAllAffectedRows));
        }

        private void BeforeMutation(CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();
            MutationCalls++;
            if (MutationException is not null)
            {
                throw MutationException;
            }
        }

        private ManualFinancialMutationReceipt Receipt(
            ManualFinancialMutationKind kind,
            ManualFinancialScreenKind screen,
            string stockName,
            int affectedRows) =>
            ReceiptOverride?.Invoke(kind, screen, stockName, affectedRows) ?? new(
                Guid.NewGuid(),
                kind,
                screen,
                stockName,
                DateTimeOffset.UtcNow,
                affectedRows);
    }
}
