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

namespace MBN_STOCK_WEBVIEW.LegacyApplication.Tests;

public sealed class LegacyManualListsWorkflowTests
{
    [Fact]
    public void Construction_is_side_effect_free_and_snapshot_has_no_file_or_stock_code_authority()
    {
        var store = new FakeStore();
        var workflow = new LegacyManualListsWorkflow(store, new FakeLookup());

        var snapshot = workflow.Current;

        Assert.Equal(0, store.ReadNetCalls);
        Assert.Equal(0, store.ReadViCalls);
        Assert.Equal(0, store.WriteNetCalls);
        Assert.Equal(0, store.WriteViCalls);
        Assert.DoesNotContain(
            typeof(LegacyManualListsSnapshot).GetProperties(),
            property => property.Name.Contains("Path", StringComparison.OrdinalIgnoreCase) ||
                property.Name.Contains("Version", StringComparison.OrdinalIgnoreCase));
        Assert.DoesNotContain(
            typeof(LegacyManualViItemView).GetProperties(),
            property => property.Name.Contains("Code", StringComparison.OrdinalIgnoreCase));
        Assert.True(snapshot.IsAvailable);
    }

    [Fact]
    public async Task FSell_saves_once_rereads_exact_five_rows_and_materializes_s5025()
    {
        var store = new FakeStore();
        var workflow = new LegacyManualListsWorkflow(store, new FakeLookup());
        await workflow.OpenNetSellAsync(S5025ManualAudience.Foreign);
        var row = workflow.Current.NetRows[0];
        workflow.SetNetSellCell(row.RowId, LegacyManualNetSellField.LeftName, "Alpha");

        var saved = await workflow.SaveNetSellAsync();
        var draft = workflow.CreateNetSellPlaylistDraft();

        Assert.Equal(1, store.WriteNetCalls);
        Assert.Equal(2, store.ReadNetCalls);
        Assert.True(saved.NetRowsAreFresh);
        Assert.Equal("Alpha", saved.NetRows[0].LeftName);
        Assert.Equal("5025", draft.SceneAlias);
        Assert.Equal("FOREIGN", draft.Selection.GroupCode);
        Assert.Equal("MANUAL_NET_SELL", draft.Selection.GraphicType);
        Assert.Equal("1/1", draft.PageText);
    }

    [Fact]
    public async Task FSell_ambiguous_failure_uses_one_readback_and_quarantines_on_mismatch()
    {
        var store = new FakeStore { ThrowNetWrite = true, PersistBeforeNetThrow = false };
        var workflow = new LegacyManualListsWorkflow(store, new FakeLookup());
        await workflow.OpenNetSellAsync(S5025ManualAudience.Individual);
        workflow.SetNetSellCell(
            workflow.Current.NetRows[0].RowId,
            LegacyManualNetSellField.LeftName,
            "Changed");

        var result = await workflow.SaveNetSellAsync();
        var second = await workflow.SaveNetSellAsync();

        Assert.True(result.WritesQuarantined);
        Assert.True(second.WritesQuarantined);
        Assert.Equal(1, store.WriteNetCalls);
        Assert.Equal(2, store.ReadNetCalls);
        Assert.False(result.CanSave);
    }

    [Fact]
    public async Task VI_search_uses_opaque_ids_and_save_binds_fresh_versioned_s5074_reference()
    {
        var store = new FakeStore();
        var workflow = new LegacyManualListsWorkflow(store, new FakeLookup());
        await workflow.OpenViAsync();
        var searched = await workflow.SearchViAsync("삼성");
        var result = Assert.Single(searched.SearchResults);
        Assert.DoesNotContain("005930", result.ResultId, StringComparison.Ordinal);
        var selected = workflow.SelectViSearchResult(result.ResultId);
        Assert.Equal(result.ResultId, selected.SelectedSearchResultId);
        Assert.Empty(selected.ViItems);
        workflow.AddViSearchResult(result.ResultId);

        var saved = await workflow.SaveViAsync();
        var draft = workflow.CreateViPlaylistDraft();

        Assert.Equal(1, store.WriteViCalls);
        Assert.Equal(3, store.ReadViCalls);
        Assert.True(saved.ViItemsAreFresh);
        Assert.Equal(1, saved.ViPageCount);
        Assert.Equal("5074", draft.SceneAlias);
        Assert.Equal("PAGED_VI", draft.Selection.GroupCode);
        Assert.Equal("@MBN_VI_SNAPSHOT_V1", draft.Selection.Subject);
        Assert.Equal(store.Vi.RowVersion, draft.Selection.DataCode);
        Assert.DoesNotContain("005930", draft.StockName, StringComparison.Ordinal);
    }

    [Fact]
    public async Task VI_delete_all_requires_MmoneyCoder_confirmation_before_changing_the_draft()
    {
        var store = new FakeStore
        {
            Vi = new LegacyManualViStoreSnapshot(
                [new LegacyManualViStoredItem("P005930", "삼성전자")],
                new string('a', 64))
        };
        var lookup = new FakeLookup();
        var controller = new LegacyOperatorController(
            lookup,
            manualListsWorkflow: new LegacyManualListsWorkflow(store, lookup));
        await controller.OpenManualViAsync();

        var requested = controller.DeleteAllManualViItems();

        Assert.Equal("모두 삭제하겠습니까?", requested.Dialog?.Message);
        Assert.Equal("MmoneyCoder", requested.Dialog?.Caption);
        Assert.True(requested.Dialog?.IsConfirmation);
        Assert.Single(requested.ManualLists!.ViItems);

        var canceled = controller.CancelDialog();
        Assert.Null(canceled.Dialog);
        Assert.Single(canceled.ManualLists!.ViItems);

        controller.DeleteAllManualViItems();
        var confirmed = await controller.ConfirmDialogAsync();

        Assert.Null(confirmed.Dialog);
        Assert.Empty(confirmed.ManualLists!.ViItems);
    }

    [Fact]
    public async Task VI_blank_search_loads_the_complete_lookup_generation()
    {
        var store = new FakeStore();
        var lookup = new FakeLookup
        {
            DisplayNames = ["삼성전자", "카카오"],
            MasterCatalog =
            [
                new LegacyStockIdentity(
                    LegacyDomesticStockMarket.Kospi,
                    "삼성전자",
                    "005930"),
                new LegacyStockIdentity(
                    LegacyDomesticStockMarket.Kosdaq,
                    "카카오",
                    "035720")
            ]
        };
        var workflow = new LegacyManualListsWorkflow(store, lookup);
        await workflow.OpenViAsync();

        var searched = await workflow.SearchViAsync("   ");

        Assert.Equal(string.Empty, lookup.LastQuery);
        Assert.Equal(2, searched.SearchResults.Count);
        Assert.False(searched.SearchIsTruncated);
        Assert.Equal(LegacyOperatorStatusKind.Information, searched.StatusKind);
    }

    [Fact]
    public async Task VI_search_keeps_a_finite_ten_thousand_row_ui_boundary_and_warns()
    {
        var store = new FakeStore();
        var lookup = new FakeLookup
        {
            DisplayNames = Enumerable.Repeat(
                "삼성전자",
                LegacyManualListsWorkflow.MaximumSearchResults + 1).ToArray()
        };
        var workflow = new LegacyManualListsWorkflow(store, lookup);
        await workflow.OpenViAsync();

        var searched = await workflow.SearchViAsync(string.Empty);

        Assert.Equal(10_000, searched.SearchResults.Count);
        Assert.True(searched.SearchIsTruncated);
        Assert.Equal(LegacyOperatorStatusKind.Warning, searched.StatusKind);
        Assert.Contains("10,000", searched.StatusMessage, StringComparison.Ordinal);
    }

    [Fact]
    public async Task VI_stale_preflight_never_writes_or_retries()
    {
        var store = new FakeStore();
        var workflow = new LegacyManualListsWorkflow(store, new FakeLookup());
        await workflow.OpenViAsync();
        var searched = await workflow.SearchViAsync("삼성");
        workflow.AddViSearchResult(searched.SearchResults.Single().ResultId);
        store.Vi = store.Vi with { RowVersion = new string('b', 64) };

        var result = await workflow.SaveViAsync();

        Assert.Equal(0, store.WriteViCalls);
        Assert.False(result.ViItemsAreFresh);
        Assert.False(result.WritesQuarantined);
        Assert.Contains("변경", result.StatusMessage, StringComparison.Ordinal);
    }

    [Fact]
    public async Task Import_outcome_unknown_latches_process_workflow_quarantine()
    {
        var store = new FakeStore { ImportOutcomeUnknown = true };
        var workflow = new LegacyManualListsWorkflow(store, new FakeLookup());

        var result = await workflow.ImportAsync();

        Assert.True(result.WritesQuarantined);
        Assert.False(result.CanSave);
        Assert.Equal(1, store.ImportCalls);
    }

    [Fact]
    public async Task Operator_controller_appends_only_native_materialized_manual_rows()
    {
        var store = new FakeStore();
        var lookup = new FakeLookup();
        var controller = new LegacyOperatorController(
            lookup,
            manualListsWorkflow: new LegacyManualListsWorkflow(store, lookup));
        await controller.OpenManualNetSellAsync(S5025ManualAudience.Institution);
        var row = controller.Current.ManualLists!.NetRows[0];
        controller.SetManualNetSellCell(
            row.RowId,
            LegacyManualNetSellField.RightAmount,
            "-3,000");
        await controller.SaveManualNetSellAsync();

        var result = controller.AddManualNetSellPlaylistRow();
        var playlist = Assert.Single(result.Playlist);

        Assert.Equal("5025", playlist.SceneAlias);
        Assert.Equal("INSTITUTION", playlist.PlayoutSelection!.GroupCode);
        Assert.Equal("MANUAL_NET_SELL", playlist.PlayoutSelection.GraphicType);
        Assert.Equal("1/1", playlist.PageText);
    }

    [Fact]
    public async Task Original_ok_saves_verifies_adds_one_cut_and_closes_the_editor()
    {
        var store = new FakeStore();
        var lookup = new FakeLookup();
        var controller = new LegacyOperatorController(
            lookup,
            manualListsWorkflow: new LegacyManualListsWorkflow(store, lookup));
        await controller.OpenManualNetSellAsync(S5025ManualAudience.Foreign);

        var result = await controller.ConfirmManualListsAsync();

        Assert.Equal(1, store.WriteNetCalls);
        Assert.Equal(2, store.ReadNetCalls);
        Assert.False(result.ManualLists!.IsOpen);
        var row = Assert.Single(result.Playlist);
        Assert.Equal("5025", row.SceneAlias);
        Assert.Equal("FOREIGN", row.PlayoutSelection!.GroupCode);
    }

    [Fact]
    public async Task Original_ok_on_unknown_write_keeps_editor_open_and_never_adds_a_cut()
    {
        var store = new FakeStore
        {
            ThrowNetWrite = true,
            PersistBeforeNetThrow = false
        };
        var lookup = new FakeLookup();
        var controller = new LegacyOperatorController(
            lookup,
            manualListsWorkflow: new LegacyManualListsWorkflow(store, lookup));
        await controller.OpenManualNetSellAsync(S5025ManualAudience.Individual);
        var row = controller.Current.ManualLists!.NetRows[0];
        controller.SetManualNetSellCell(
            row.RowId,
            LegacyManualNetSellField.LeftAmount,
            "999");

        var result = await controller.ConfirmManualListsAsync();

        Assert.Equal(1, store.WriteNetCalls);
        Assert.Empty(result.Playlist);
        Assert.True(result.ManualLists!.IsOpen);
        Assert.True(result.ManualLists.WritesQuarantined);
    }

    [Theory]
    [InlineData("fixed-245", S5025ManualAudience.Individual)]
    [InlineData("fixed-246", S5025ManualAudience.Foreign)]
    [InlineData("fixed-247", S5025ManualAudience.Institution)]
    public async Task Original_manual_net_sell_tree_leaves_open_the_matching_editor(
        string actionId,
        S5025ManualAudience audience)
    {
        var store = new FakeStore();
        var lookup = new FakeLookup();
        var controller = new LegacyOperatorController(
            lookup,
            manualListsWorkflow: new LegacyManualListsWorkflow(store, lookup));
        await controller.SelectTabAsync(LegacyOperatorTabId.Index);

        var result = await controller.ActivateFixedActionAsync(actionId);

        Assert.True(result.ManualLists!.IsOpen);
        Assert.Equal(LegacyManualListScreen.NetSell, result.ManualLists.Screen);
        Assert.Equal(audience, result.ManualLists.Audience);
        Assert.Empty(result.Playlist);
    }

    [Theory]
    [InlineData("fixed-262")]
    [InlineData("fixed-293")]
    [InlineData("fixed-320")]
    public async Task Original_manual_vi_tree_leaves_open_the_vi_editor(string actionId)
    {
        var store = new FakeStore
        {
            Vi = new LegacyManualViStoreSnapshot(
                Enumerable.Range(1, 9)
                    .Select(index => new LegacyManualViStoredItem(
                        $"P{index:D6}",
                        $"VI종목{index}"))
                    .ToArray(),
                new string('a', 64))
        };
        var lookup = new FakeLookup();
        var controller = new LegacyOperatorController(
            lookup,
            manualListsWorkflow: new LegacyManualListsWorkflow(store, lookup));
        await controller.SelectTabAsync(LegacyOperatorTabId.Index);

        var result = await controller.ActivateFixedActionAsync(actionId);

        Assert.True(result.ManualLists!.IsOpen);
        Assert.Equal(LegacyManualListScreen.Vi, result.ManualLists.Screen);
        Assert.Equal(9, result.ManualLists.ViItems.Count);
        Assert.Equal(2, result.ManualLists.ViPageCount);
        Assert.True(result.ManualLists.ViItemsAreFresh);
        Assert.True(result.ManualLists.CanSave);
        Assert.True(result.ManualLists.CanMaterializePlaylist);
        Assert.Empty(result.Playlist);
    }

    [Fact]
    public async Task Fixed_parent_stages_three_FSell_children_and_commits_once_in_original_order()
    {
        var store = new FakeStore();
        var lookup = new FakeLookup();
        var controller = new LegacyOperatorController(
            lookup,
            manualListsWorkflow: new LegacyManualListsWorkflow(store, lookup));
        await controller.SelectTabAsync(LegacyOperatorTabId.Index);
        var section = LegacyFixedActionCatalog.Default
            .GetSections(LegacyFixedMarket.Index)[6];

        var first = await controller.ActivateFixedSection(6);

        Assert.Empty(first.Playlist);
        Assert.Equal("fixed-245", first.FixedSectionBatch!.PendingActionId);
        Assert.Equal(1, first.FixedSectionBatch.ManualStepNumber);
        Assert.Equal(3, first.FixedSectionBatch.ManualStepCount);
        Assert.Equal(S5025ManualAudience.Individual, first.ManualLists!.Audience);

        var second = await controller.ConfirmManualListsAsync();
        Assert.Empty(second.Playlist);
        Assert.Equal("fixed-246", second.FixedSectionBatch!.PendingActionId);
        Assert.Equal(S5025ManualAudience.Foreign, second.ManualLists!.Audience);

        var third = await controller.ConfirmManualListsAsync();
        Assert.Empty(third.Playlist);
        Assert.Equal("fixed-247", third.FixedSectionBatch!.PendingActionId);
        Assert.Equal(S5025ManualAudience.Institution, third.ManualLists!.Audience);

        var completed = await controller.ConfirmManualListsAsync();

        Assert.Null(completed.FixedSectionBatch);
        Assert.False(completed.ManualLists!.IsOpen);
        Assert.Equal(3, store.WriteNetCalls);
        Assert.Equal(section.Actions.Count, completed.Playlist.Count);
        Assert.Equal(
            section.Actions.Select(action => action.Label),
            completed.Playlist.Select(row => row.RawCutLabel));
        Assert.Equal(
            new[] { "INDIVIDUAL", "FOREIGN", "INSTITUTION" },
            completed.Playlist.TakeLast(3).Select(row => row.PlayoutSelection!.GroupCode));
    }

    [Fact]
    public async Task Fixed_parent_cancel_discards_all_playlist_staging_but_keeps_known_manual_save()
    {
        var store = new FakeStore();
        var lookup = new FakeLookup();
        var controller = new LegacyOperatorController(
            lookup,
            manualListsWorkflow: new LegacyManualListsWorkflow(store, lookup));
        await controller.SelectTabAsync(LegacyOperatorTabId.Index);
        await controller.ActivateFixedSection(6);
        await controller.ConfirmManualListsAsync();

        var cancelled = controller.CloseManualLists();

        Assert.Equal(1, store.WriteNetCalls);
        Assert.Empty(cancelled.Playlist);
        Assert.Null(cancelled.FixedSectionBatch);
        Assert.False(cancelled.ManualLists!.IsOpen);
        Assert.Equal(LegacyOperatorStatusKind.Warning, cancelled.StatusKind);
        Assert.Contains("아무 항목도 추가하지 않았습니다", cancelled.StatusMessage,
            StringComparison.Ordinal);
    }

    [Fact]
    public async Task Fixed_parent_outcome_unknown_keeps_same_step_and_never_retries_or_appends()
    {
        var store = new FakeStore
        {
            ThrowNetWrite = true,
            PersistBeforeNetThrow = false
        };
        var lookup = new FakeLookup();
        var controller = new LegacyOperatorController(
            lookup,
            manualListsWorkflow: new LegacyManualListsWorkflow(store, lookup));
        await controller.SelectTabAsync(LegacyOperatorTabId.Index);
        var opened = await controller.ActivateFixedSection(6);
        controller.SetManualNetSellCell(
            opened.ManualLists!.NetRows[0].RowId,
            LegacyManualNetSellField.LeftAmount,
            "999");

        var unknown = await controller.ConfirmManualListsAsync();
        var blockedRepeat = await controller.ConfirmManualListsAsync();

        Assert.Equal(1, store.WriteNetCalls);
        Assert.Empty(unknown.Playlist);
        Assert.Empty(blockedRepeat.Playlist);
        Assert.Equal("fixed-245", blockedRepeat.FixedSectionBatch!.PendingActionId);
        Assert.True(blockedRepeat.ManualLists!.IsOpen);
        Assert.True(blockedRepeat.ManualLists.WritesQuarantined);
    }

    [Fact]
    public async Task Fixed_parent_VI_child_resumes_and_commits_at_its_original_child_position()
    {
        var store = new FakeStore();
        var lookup = new FakeLookup();
        var pagePlans = new FiveRowPagePlanProvider();
        var controller = new LegacyOperatorController(
            lookup,
            manualListsWorkflow: new LegacyManualListsWorkflow(store, lookup),
            fixedPagePlanProvider: pagePlans);
        await controller.SelectTabAsync(LegacyOperatorTabId.Index);
        var section = LegacyFixedActionCatalog.Default
            .GetSections(LegacyFixedMarket.Index)[7];
        var manualIndex = section.Actions.ToList().FindIndex(action => action.Id == "fixed-262");
        var opened = await controller.ActivateFixedSection(7);
        var searched = await controller.SearchManualViAsync("삼성");
        controller.AddManualViSearchResult(
            Assert.Single(searched.ManualLists!.SearchResults).ResultId);

        var completed = await controller.ConfirmManualListsAsync();

        Assert.Null(completed.FixedSectionBatch);
        Assert.Equal(section.Actions.Count, completed.Playlist.Count);
        Assert.Equal(
            section.Actions.Take(manualIndex).Select(action => action.Label),
            completed.Playlist.Take(manualIndex).Select(row => row.RawCutLabel));
        Assert.Equal(
            section.Actions.Skip(manualIndex + 1).Select(action => action.Label),
            completed.Playlist.Skip(manualIndex + 1).Select(row => row.RawCutLabel));
        Assert.Equal("5074", completed.Playlist[manualIndex].SceneAlias);
        Assert.Equal("PAGED_VI", completed.Playlist[manualIndex].PlayoutSelection!.GroupCode);
        Assert.All(
            completed.Playlist.Where((_, index) => index != manualIndex),
            row => Assert.Equal("1/3", row.PageText));
        Assert.Equal(section.Actions.Count - 1, pagePlans.CallCount);
        Assert.Equal(1, store.WriteViCalls);
    }

    [Fact]
    public async Task Fixed_parent_modal_contract_blocks_tab_and_manual_target_switches()
    {
        var store = new FakeStore();
        var lookup = new FakeLookup();
        var controller = new LegacyOperatorController(
            lookup,
            manualListsWorkflow: new LegacyManualListsWorkflow(store, lookup));
        await controller.SelectTabAsync(LegacyOperatorTabId.Index);
        await controller.ActivateFixedSection(6);

        var obsoleteSave = await controller.SaveManualNetSellAsync();
        var obsoleteAdd = controller.AddManualNetSellPlaylistRow();
        var switchedManual = await controller.OpenManualNetSellAsync(
            S5025ManualAudience.Institution);
        var switchedTab = await controller.SelectTabAsync(LegacyOperatorTabId.Exchange);

        Assert.Equal(0, store.WriteNetCalls);
        Assert.Empty(obsoleteSave.Playlist);
        Assert.Empty(obsoleteAdd.Playlist);
        Assert.Equal(S5025ManualAudience.Individual, switchedManual.ManualLists!.Audience);
        Assert.Equal(S5025ManualAudience.Individual, switchedTab.ManualLists!.Audience);
        Assert.Equal(
            LegacyOperatorTabId.Index,
            Assert.Single(switchedTab.Tabs!, tab => tab.IsActive).Id);
        Assert.Empty(switchedTab.Playlist);
    }

    private sealed class FiveRowPagePlanProvider : ILegacyScenePagePlanProvider
    {
        public int CallCount { get; private set; }

        public Task<LegacyScenePagePlan> CreatePagePlanAsync(
            LegacyPlaylistEntry entry,
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            CallCount++;
            Assert.Equal("5074", entry.CutCode);
            return Task.FromResult(new LegacyScenePagePlan(
                "s5074",
                11,
                ScenePageSize.Five,
                3,
                11,
                false));
        }
    }

    private sealed class FakeLookup : ILegacyStockLookup
    {
        public IReadOnlyList<string> DisplayNames { get; init; } = ["삼성전자"];

        public IReadOnlyList<LegacyStockIdentity> MasterCatalog { get; init; } =
        [
            new LegacyStockIdentity(
                LegacyDomesticStockMarket.Kospi,
                "삼성전자",
                "005930")
        ];

        public string? LastQuery { get; private set; }

        public Task<LegacyStockSearchData> SearchAsync(
            string query,
            CancellationToken cancellationToken = default)
        {
            LastQuery = query;
            return Task.FromResult(new LegacyStockSearchData(
                query,
                DisplayNames,
                MasterCatalog));
        }
    }

    private sealed class FakeStore : ILegacyManualListStore
    {
        private readonly Dictionary<S5025ManualAudience, IReadOnlyList<S5025TrustedManualRow>>
            _net = Enum.GetValues<S5025ManualAudience>().ToDictionary(
                audience => audience,
                _ => (IReadOnlyList<S5025TrustedManualRow>)Rows());

        public LegacyManualListStoreAvailability Availability { get; } =
            new(true, "READY", null);

        public LegacyManualViStoreSnapshot Vi { get; set; } = new(
            Array.Empty<LegacyManualViStoredItem>(),
            new string('a', 64));

        public int ReadNetCalls { get; private set; }
        public int WriteNetCalls { get; private set; }
        public int ReadViCalls { get; private set; }
        public int WriteViCalls { get; private set; }
        public int ImportCalls { get; private set; }
        public bool ThrowNetWrite { get; init; }
        public bool PersistBeforeNetThrow { get; init; }
        public bool ImportOutcomeUnknown { get; init; }

        public Task<IReadOnlyList<S5025TrustedManualRow>> ReadNetSellAsync(
            S5025ManualAudience audience,
            CancellationToken cancellationToken = default)
        {
            ReadNetCalls++;
            return Task.FromResult(_net[audience]);
        }

        public Task WriteNetSellAsync(
            S5025ManualAudience audience,
            IReadOnlyList<S5025TrustedManualRow> rows,
            CancellationToken cancellationToken = default)
        {
            WriteNetCalls++;
            if (!ThrowNetWrite || PersistBeforeNetThrow)
            {
                _net[audience] = rows.ToArray();
            }

            return ThrowNetWrite
                ? Task.FromException(new IOException("ambiguous"))
                : Task.CompletedTask;
        }

        public Task<LegacyManualViStoreSnapshot> ReadViAsync(
            CancellationToken cancellationToken = default)
        {
            ReadViCalls++;
            return Task.FromResult(Vi);
        }

        public Task<LegacyManualViWriteReceipt> WriteViAsync(
            IReadOnlyList<LegacyManualViStoredItem> items,
            CancellationToken cancellationToken = default)
        {
            WriteViCalls++;
            Vi = new LegacyManualViStoreSnapshot(items.ToArray(), new string('c', 64));
            return Task.FromResult(new LegacyManualViWriteReceipt(Vi, true));
        }

        public Task<LegacyManualImportReceipt> ImportAsync(
            CancellationToken cancellationToken = default)
        {
            ImportCalls++;
            return ImportOutcomeUnknown
                ? Task.FromException<LegacyManualImportReceipt>(
                    new LegacyManualImportStoreException("ROLLBACK_FAILED", true))
                : Task.FromResult(new LegacyManualImportReceipt(
                    1,
                    new string('d', 64),
                    DateTimeOffset.UnixEpoch,
                    15,
                    0));
        }

        private static S5025TrustedManualRow[] Rows() =>
            Enumerable.Range(1, 5).Select(index => new S5025TrustedManualRow(
                $"L{index}",
                index.ToString(System.Globalization.CultureInfo.InvariantCulture),
                $"R{index}",
                (-index).ToString(System.Globalization.CultureInfo.InvariantCulture))).ToArray();
    }
}
