using MMoneyCoderSharp.Data; namespace MBN_STOCK_WEBVIEW.LegacyApplication.Tests; public sealed class LegacyExpertWorkflowTests { private static readonly ExpertSelectionIdentity Alpha = new("0001", "강전문"); private static readonly ExpertSelectionIdentity Beta = new("0002", "김전문"); [Fact] public async Task SearchSelectPreview_MaterializesClosedS5074DraftFromActionIdOnly() { var service = new DelegateExpertService { Search = (query, _, _) => Task.FromResult(SearchResult(query, Alpha, Beta)), Preview = (identity, _, _) => Task.FromResult( PreviewResult(identity, Recommendations(6))) }; var controller = new LegacyExpertWorkflowController(service); var search = await controller.SearchAsync(" 전문 "); var preview = await controller.SelectExpertAsync(search.Search.Results[1].SelectionId); var draft = controller.MaterializeSelectedAction("five-row-current"); Assert.Equal("전문", service.SearchQuery); Assert.Equal(500, service.SearchMaximum); Assert.Equal(100, service.PreviewMaximum); Assert.Equal("김전문", preview.SelectedExpert?.ExpertName); Assert.Equal(6, preview.Preview.Items.Count); Assert.Equal(2, preview.Preview.PageCount); Assert.True(Assert.Single(preview.Actions).IsAvailable); Assert.Equal("s5074", draft.BuilderKey); Assert.Equal("5074", draft.CutCode); Assert.Equal(new[] { "5074" }, draft.Aliases); Assert.Equal("PAGED_EXPERT", draft.Selection.GroupCode); Assert.Equal("김전문", draft.Selection.Subject); Assert.Equal("INPUT_ORDER", draft.Selection.GraphicType); Assert.Equal("CURRENT", draft.Selection.Subtype); Assert.Equal("0002", draft.Selection.DataCode); Assert.Equal(5, draft.PageSize); Assert.Equal(2, draft.PageCount); Assert.Equal(20, draft.MaximumPageCount); Assert.Equal("1/2", draft.PageText); Assert.Equal(draft.DraftId, draft.ToLegacyPlaylistEntry().EntryId); Assert.Equal("legacy-expert-workflow", draft.Source); var method = typeof(LegacyExpertWorkflowController).GetMethod( nameof(LegacyExpertWorkflowController.MaterializeSelectedAction)); Assert.NotNull(method); Assert.Collection( method.GetParameters(), parameter => Assert.Equal("actionId", parameter.Name)); Assert.Throws( () => controller.MaterializeSelectedAction("5074-caller-override")); } [Theory] [InlineData(1, false, 1)] [InlineData(5, false, 1)] [InlineData(6, false, 2)] [InlineData(100, true, 20)] public async Task PageN_UsesFiveRowsAndNeverExceedsTwentyPages( int itemCount, bool truncated, int expectedPageCount) { var service = new DelegateExpertService { Search = (query, _, _) => Task.FromResult(SearchResult(query, Alpha)), Preview = (identity, _, _) => Task.FromResult( PreviewResult(identity, Recommendations(itemCount), truncated)) }; var controller = new LegacyExpertWorkflowController(service); var search = await controller.SearchAsync(string.Empty); var selected = await controller.SelectExpertAsync( search.Search.Results[0].SelectionId); var draft = controller.MaterializeSelectedAction( LegacyExpertWorkflowController.ActionId); Assert.Equal(expectedPageCount, selected.Preview.PageCount); Assert.Equal(expectedPageCount, draft.PageCount); Assert.InRange(draft.PageCount, 1, 20); Assert.Equal(truncated, draft.PreviewTruncated); } [Fact] public async Task EmptyRecommendationPreview_PreservesOriginalOneOverZeroCut() { var service = new DelegateExpertService { Search = (query, _, _) => Task.FromResult(SearchResult(query, Alpha)), Preview = (identity, _, _) => Task.FromResult( PreviewResult(identity, [])) }; var controller = new LegacyExpertWorkflowController(service); var search = await controller.SearchAsync(string.Empty); var selected = await controller.SelectExpertAsync( search.Search.Results[0].SelectionId); Assert.Equal(LegacyExpertRequestStatus.Ready, selected.Preview.Status); Assert.Empty(selected.Preview.Items); Assert.Equal(0, selected.Preview.PageCount); Assert.True(Assert.Single(selected.Actions).IsAvailable); var draft = controller.MaterializeSelectedAction( LegacyExpertWorkflowController.ActionId); Assert.Equal(0, draft.ItemCount); Assert.Equal(0, draft.PageCount); Assert.Equal("1/0", draft.PageText); } [Fact] public async Task LateSearchResponse_CannotReplaceNewerExpertResult() { var first = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously); var second = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously); var service = new DelegateExpertService { Search = (query, _, _) => query == "첫째" ? first.Task : second.Task }; var controller = new LegacyExpertWorkflowController(service); var firstRequest = controller.SearchAsync("첫째"); var secondRequest = controller.SearchAsync("둘째"); second.SetResult(SearchResult("둘째", Beta)); var newest = await secondRequest; first.SetResult(SearchResult("첫째", Alpha)); var staleCompletion = await firstRequest; Assert.Equal("둘째", newest.Search.Query); Assert.Equal("김전문", Assert.Single(newest.Search.Results).ExpertName); Assert.Equal("둘째", staleCompletion.Search.Query); Assert.Equal("김전문", Assert.Single(staleCompletion.Search.Results).ExpertName); Assert.Equal("둘째", controller.Current.Search.Query); } [Fact] public async Task LatePreviewResponse_CannotReplaceNewerExpertSelection() { var alphaPreview = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously); var betaPreview = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously); var service = new DelegateExpertService { Search = (query, _, _) => Task.FromResult(SearchResult(query, Alpha, Beta)), Preview = (identity, _, _) => identity == Alpha ? alphaPreview.Task : betaPreview.Task }; var controller = new LegacyExpertWorkflowController(service); var search = await controller.SearchAsync(string.Empty); var firstSelection = controller.SelectExpertAsync(search.Search.Results[0].SelectionId); var secondSelection = controller.SelectExpertAsync(search.Search.Results[1].SelectionId); betaPreview.SetResult(PreviewResult(Beta, Recommendations(2, "B"))); var newest = await secondSelection; alphaPreview.SetResult(PreviewResult(Alpha, Recommendations(1, "A"))); var staleCompletion = await firstSelection; Assert.Equal("김전문", newest.SelectedExpert?.ExpertName); Assert.Equal(2, newest.Preview.Items.Count); Assert.Equal("김전문", staleCompletion.SelectedExpert?.ExpertName); Assert.Equal(2, staleCompletion.Preview.Items.Count); Assert.All(staleCompletion.Preview.Items, item => Assert.StartsWith("B", item.StockCode, StringComparison.Ordinal)); } [Fact] public async Task StrictExpertIdentityOrderAndPreviewIdentity_AreRevalidated() { var unsorted = new DelegateExpertService { Search = (query, _, _) => Task.FromResult(SearchResult(query, Beta, Alpha)) }; var unsortedController = new LegacyExpertWorkflowController(unsorted); await Assert.ThrowsAsync( () => unsortedController.SearchAsync(string.Empty)); Assert.Equal( LegacyExpertRequestStatus.Error, unsortedController.Current.Search.Status); var wrongPreview = new DelegateExpertService { Search = (query, _, _) => Task.FromResult(SearchResult(query, Alpha)), Preview = (_, _, _) => Task.FromResult( PreviewResult(Beta, Recommendations(1))) }; var wrongController = new LegacyExpertWorkflowController(wrongPreview); var search = await wrongController.SearchAsync(string.Empty); await Assert.ThrowsAsync( () => wrongController.SelectExpertAsync(search.Search.Results[0].SelectionId)); Assert.Equal( LegacyExpertRequestStatus.Error, wrongController.Current.Preview.Status); } [Fact] public async Task ExpertSnapshots_AreImmutableAcrossLaterQueries() { var service = new DelegateExpertService { Search = (query, _, _) => Task.FromResult( query == "A" ? SearchResult(query, Alpha) : SearchResult(query, Beta)) }; var controller = new LegacyExpertWorkflowController(service); var before = await controller.SearchAsync("A"); var after = await controller.SearchAsync("B"); Assert.Equal("강전문", Assert.Single(before.Search.Results).ExpertName); Assert.Equal("김전문", Assert.Single(after.Search.Results).ExpertName); var rows = Assert.IsAssignableFrom>( before.Search.Results); Assert.Throws(() => rows.Clear()); var actions = Assert.IsAssignableFrom>( before.Actions); Assert.Throws(() => actions.Add(before.Actions[0])); } private static ExpertSearchResult SearchResult( string query, params ExpertSelectionIdentity[] identities) => new( query, DateTimeOffset.Parse("2026-07-15T00:00:00+09:00"), identities.Select(identity => new ExpertSelectorItem(identity, DataSourceKind.Oracle)).ToArray(), false); private static ExpertPreviewResult PreviewResult( ExpertSelectionIdentity identity, IReadOnlyList items, bool truncated = false) => new( identity, DateTimeOffset.Parse("2026-07-15T00:00:00+09:00"), items, truncated); private static IReadOnlyList Recommendations( int count, string prefix = "S") => Enumerable.Range(0, count) .Select(index => new ExpertRecommendationPreview( index, $"{prefix}{index:D4}", $"추천종목{prefix}{index:D4}", 10_000m + index)) .ToArray(); private sealed class DelegateExpertService : IExpertSelectionService { public Func> Search { get; init; } = (query, _, _) => Task.FromResult(SearchResult(query)); public Func> Preview { get; init; } = (identity, _, _) => Task.FromResult(PreviewResult(identity, [])); public string? SearchQuery { get; private set; } public int SearchMaximum { get; private set; } public int PreviewMaximum { get; private set; } public Task SearchAsync( string query = "", int maximumResults = LegacyExpertSelectionService.DefaultMaximumResults, CancellationToken cancellationToken = default) { SearchQuery = query; SearchMaximum = maximumResults; return Search(query, maximumResults, cancellationToken); } public Task PreviewAsync( ExpertSelectionIdentity identity, int maximumItems = LegacyExpertSelectionService.DefaultMaximumPreviewItems, CancellationToken cancellationToken = default) { PreviewMaximum = maximumItems; return Preview(identity, maximumItems, cancellationToken); } } }