using MMoneyCoderSharp.Data; namespace MBN_STOCK_WEBVIEW.LegacyApplication.Tests; public sealed class LegacyTabActivationParityTests { [Fact] public async Task ThemeEachActivation_LoadsOriginalEmptyQueryOncePerActivation() { // Original evidence: Control/UC4.cs UC4_Load starts timer1 and its first // timer1_Tick calls GetThemeByKeyword with the initially empty txts value. var service = new CountingThemeService(); var controller = new LegacyOperatorController( Lookup(), themeWorkflow: new LegacyThemeWorkflow(service)); var first = await controller.SelectTabAsync(LegacyOperatorTabId.Theme); Assert.Equal(string.Empty, service.Queries.Single()); var firstResultId = Assert.Single(first.Theme!.SearchResults).ResultId; controller.SetThemeSort("GAIN_DESC"); var selected = await controller.SelectThemeAsync(firstResultId); Assert.Equal(LegacyThemeSort.GainRateDescending, selected.Theme!.Sort); Assert.NotNull(selected.Theme.SelectedTheme); await controller.SelectTabAsync(LegacyOperatorTabId.Theme); Assert.Single(service.Queries); await controller.SelectTabAsync(LegacyOperatorTabId.Index); var second = await controller.SelectTabAsync(LegacyOperatorTabId.Theme); Assert.Collection( service.Queries, query => Assert.Equal(string.Empty, query), query => Assert.Equal(string.Empty, query)); Assert.Single(second.Theme!.SearchResults); Assert.Equal(LegacyThemeSort.InputOrder, second.Theme.Sort); Assert.Null(second.Theme.SelectedResultId); Assert.Null(second.Theme.SelectedTheme); Assert.Empty(second.Theme.PreviewItems); } [Fact] public async Task ExpertEachActivation_LoadsOriginalFullListOncePerActivation() { // Original evidence: Control/UC6.cs UC6_Load calls Data_Load, whose query // reads the complete EXPERT_LIST without a filter. var service = new CountingExpertService(); var controller = new LegacyOperatorController( Lookup(), expertWorkflow: new LegacyExpertWorkflowController(service)); var first = await controller.SelectTabAsync(LegacyOperatorTabId.Expert); Assert.Equal(string.Empty, service.Queries.Single()); Assert.Single(first.Expert!.Search.Results); await controller.SelectTabAsync(LegacyOperatorTabId.Expert); Assert.Single(service.Queries); await controller.SelectTabAsync(LegacyOperatorTabId.Index); var second = await controller.SelectTabAsync(LegacyOperatorTabId.Expert); Assert.Collection( service.Queries, query => Assert.Equal(string.Empty, query), query => Assert.Equal(string.Empty, query)); Assert.Single(second.Expert!.Search.Results); } [Fact] public async Task DragSwapThatActivatesTheme_UsesTheSameNativeInitialLoad() { var service = new CountingThemeService(); var controller = new LegacyOperatorController( Lookup(), themeWorkflow: new LegacyThemeWorkflow(service)); var state = await controller.SwapTabsAsync( LegacyOperatorTabId.Theme, LegacyOperatorTabId.Exchange); Assert.Equal(string.Empty, service.Queries.Single()); Assert.Single(state.Theme!.SearchResults); Assert.Equal( LegacyOperatorTabId.Theme, Assert.Single(state.Tabs!, tab => tab.IsActive).Id); } [Fact] public async Task SupersededThemeActivation_DoesNotCommitAndRetriesWhenNeeded() { var service = new DelayedThemeService(); var controller = new LegacyOperatorController( Lookup(), themeWorkflow: new LegacyThemeWorkflow(service)); var firstActivation = controller.SelectTabAsync(LegacyOperatorTabId.Theme); await service.FirstRequestStarted.Task.WaitAsync(TimeSpan.FromSeconds(5)); controller.SelectTab(LegacyOperatorTabId.Index); service.ReleaseFirstRequest.TrySetResult(); var superseded = await firstActivation; Assert.Equal( LegacyOperatorTabId.Index, Assert.Single(superseded.Tabs!, tab => tab.IsActive).Id); var retried = await controller.SelectTabAsync(LegacyOperatorTabId.Theme); Assert.Equal(2, service.CallCount); Assert.Single(retried.Theme!.SearchResults); } [Theory] [InlineData(LegacyOperatorTabId.KospiIndustry, IndustryMarket.Kospi)] [InlineData(LegacyOperatorTabId.KosdaqIndustry, IndustryMarket.Kosdaq)] public async Task IndustryEachActivation_ReloadsAndClearsUc2Selections( LegacyOperatorTabId tabId, IndustryMarket market) { var service = new CountingIndustryService(); var controller = new LegacyOperatorController( Lookup(), industryWorkflow: new LegacyIndustrySelectionWorkflow(service)); await controller.SelectTabAsync(tabId); controller.SelectIndustry(0, doubleClick: true); var selected = controller.SelectIndustry(1, doubleClick: true); Assert.NotNull(selected.Industry!.SelectedIndustry); Assert.NotNull(selected.Industry.FirstIndustry); Assert.NotNull(selected.Industry.SecondIndustry); await controller.SelectTabAsync(tabId); Assert.Single(service.Markets); await controller.SelectTabAsync(LegacyOperatorTabId.Index); var reentered = await controller.SelectTabAsync(tabId); Assert.Equal([market, market], service.Markets); Assert.Equal(2, reentered.Industry!.Industries.Count); Assert.Null(reentered.Industry.SelectedIndex); Assert.Null(reentered.Industry.SelectedIndustry); Assert.Null(reentered.Industry.FirstIndustry); Assert.Null(reentered.Industry.SecondIndustry); } [Fact] public async Task ComparisonEachActivation_ReloadsPairsAndClearsUc3DraftAndSearch() { var store = new CountingComparisonPairStore(); var controller = new LegacyOperatorController( Lookup(), comparisonWorkflow: new LegacyComparisonWorkflowController( new StaticComparisonDataService(), store)); await controller.SelectTabAsync(LegacyOperatorTabId.Comparison); var searched = await controller.SearchComparisonDomesticAsync("semiconductor"); var results = searched.Comparison!.DomesticSearch.Results; controller.ChooseComparisonDomesticResult(results[0].SelectionId); var drafted = controller.ChooseComparisonDomesticResult(results[1].SelectionId); Assert.NotNull(drafted.Comparison!.FirstTarget); Assert.NotNull(drafted.Comparison.SecondTarget); await controller.SelectTabAsync(LegacyOperatorTabId.Comparison); Assert.Equal(1, store.ReadCount); await controller.SelectTabAsync(LegacyOperatorTabId.Index); var reentered = await controller.SelectTabAsync(LegacyOperatorTabId.Comparison); Assert.Equal(2, store.ReadCount); Assert.Equal(LegacyComparisonSearchStatus.Idle, reentered.Comparison!.DomesticSearch.Status); Assert.Empty(reentered.Comparison.DomesticSearch.Results); Assert.Equal(string.Empty, reentered.Comparison.DomesticSearch.Query); Assert.Null(reentered.Comparison.FirstTarget); Assert.Null(reentered.Comparison.SecondTarget); Assert.Empty(reentered.Comparison.SavedPairs); } [Fact] public async Task ComparisonReentry_InvalidatesPairLoadFromDiscardedUc3() { var store = new DelayedComparisonPairStore(); var controller = new LegacyOperatorController( Lookup(), comparisonWorkflow: new LegacyComparisonWorkflowController( new StaticComparisonDataService(), store)); var firstActivation = controller.SelectTabAsync(LegacyOperatorTabId.Comparison); await store.FirstReadStarted.Task.WaitAsync(TimeSpan.FromSeconds(5)); controller.SelectTab(LegacyOperatorTabId.Index); controller.SelectTab(LegacyOperatorTabId.Comparison); store.ReleaseFirstRead.TrySetResult(); await firstActivation; Assert.Equal(LegacyOperatorStatusKind.Neutral, controller.Current.Comparison!.StatusKind); Assert.Equal(string.Empty, controller.Current.Comparison.StatusMessage); Assert.Empty(controller.Current.Comparison.SavedPairs); var reloaded = await controller.SelectTabAsync(LegacyOperatorTabId.Comparison); Assert.Equal(2, store.ReadCount); Assert.Equal(LegacyOperatorStatusKind.Information, reloaded.Comparison!.StatusKind); } [Fact] public async Task OverseasStocksReentry_ResetsUc5StateAndKeepsMainMovingAverages() { var workflow = new LegacyOverseasSelectionWorkflow( new StaticOverseasIndustryService(), new StaticOverseasStockService()); var controller = new LegacyOperatorController(Lookup(), overseasWorkflow: workflow); controller.SelectTab(LegacyOperatorTabId.OverseasStocks); controller.SelectOverseasFixedIndex("nasdaq"); var industries = await controller.SearchOverseasIndustriesAsync("industry"); controller.SelectOverseasIndustry( Assert.Single(industries.Overseas!.Industry.Results).SelectionId); var stocks = await controller.SearchOverseasStocksAsync("stock"); controller.SelectOverseasStock( Assert.Single(stocks.Overseas!.Stock.Results).SelectionId); controller.SetMovingAverages(ma5: false, ma20: true); controller.SelectTab(LegacyOperatorTabId.Index); var reentered = controller.SelectTab(LegacyOperatorTabId.OverseasStocks); Assert.Equal("dow-jones", reentered.Overseas!.SelectedFixedIndex?.TargetId); Assert.Equal(LegacyOverseasSearchStatus.Idle, reentered.Overseas.Industry.Status); Assert.Equal(string.Empty, reentered.Overseas.Industry.Query); Assert.Empty(reentered.Overseas.Industry.Results); Assert.Null(reentered.Overseas.Industry.Selected); Assert.Equal(LegacyOverseasSearchStatus.Idle, reentered.Overseas.Stock.Status); Assert.Equal(string.Empty, reentered.Overseas.Stock.Query); Assert.Empty(reentered.Overseas.Stock.Results); Assert.Null(reentered.Overseas.Stock.Selected); Assert.Equal( new LegacyOverseasMovingAverageSelection(false, true), reentered.Overseas.MovingAverages); } [Fact] public async Task TradingHaltReentry_ResetsUc7QuerySelectionAndSort() { var controller = new LegacyOperatorController( Lookup(), tradingHaltWorkflow: new LegacyTradingHaltWorkflowController( new StaticTradingHaltService())); controller.SelectTab(LegacyOperatorTabId.TradingHalt); var searched = await controller.SearchTradingHaltsAsync("halted"); controller.SelectTradingHalt( Assert.Single(searched.TradingHalt!.Results).SelectionId); controller.ToggleTradingHaltSort(); controller.SetMovingAverages(ma5: false, ma20: true); controller.SelectTab(LegacyOperatorTabId.Index); var reentered = controller.SelectTab(LegacyOperatorTabId.TradingHalt); Assert.Equal(LegacyTradingHaltRequestStatus.Idle, reentered.TradingHalt!.Status); Assert.Equal(string.Empty, reentered.TradingHalt.Query); Assert.Empty(reentered.TradingHalt.Results); Assert.Null(reentered.TradingHalt.Selected); Assert.Equal(LegacyTradingHaltNameSort.None, reentered.TradingHalt.NameSort); Assert.False(reentered.TradingHalt.Ma5); Assert.True(reentered.TradingHalt.Ma20); } [Fact] public async Task OverseasReentry_InvalidatesSearchCompletionFromDiscardedUc5() { var service = new DelayedOverseasIndustryService(); var controller = new LegacyOperatorController( Lookup(), overseasWorkflow: new LegacyOverseasSelectionWorkflow( service, new StaticOverseasStockService())); controller.SelectTab(LegacyOperatorTabId.OverseasStocks); var pending = controller.SearchOverseasIndustriesAsync("old"); await service.RequestStarted.Task.WaitAsync(TimeSpan.FromSeconds(5)); controller.SelectTab(LegacyOperatorTabId.Index); controller.SelectTab(LegacyOperatorTabId.OverseasStocks); service.ReleaseRequest.TrySetResult(); await pending; Assert.Equal(LegacyOverseasSearchStatus.Idle, controller.Current.Overseas!.Industry.Status); Assert.Equal(string.Empty, controller.Current.Overseas.Industry.Query); Assert.Empty(controller.Current.Overseas.Industry.Results); } private static ILegacyStockLookup Lookup() => new StaticLookup(new LegacyStockSearchData(string.Empty, [], [])); private sealed class StaticLookup : ILegacyStockLookup { private readonly LegacyStockSearchData _data; public StaticLookup(LegacyStockSearchData data) { _data = data; } public Task SearchAsync( string text, CancellationToken cancellationToken = default) => Task.FromResult(_data); } private sealed class CountingThemeService : IThemeSelectionService { private static readonly ThemeSelectionIdentity Identity = new( ThemeMarket.Krx, ThemeSession.NotApplicable, "00000001", "Theme A"); public List Queries { get; } = []; public Task SearchAsync( string query, ThemeSession nxtSession, int maximumResults = LegacyThemeSelectionService.DefaultMaximumResults, CancellationToken cancellationToken = default) { Queries.Add(query); return Task.FromResult(CreateThemeResult(query, nxtSession)); } public Task PreviewAsync( ThemeSelectionIdentity identity, int maximumItems = LegacyThemeSelectionService.DefaultMaximumPreviewItems, CancellationToken cancellationToken = default) => Task.FromResult( new ThemePreviewResult(identity, DateTimeOffset.UtcNow, [], false)); public static ThemeSearchResult CreateThemeResult( string query, ThemeSession nxtSession) => new( query, nxtSession, DateTimeOffset.UtcNow, [new ThemeSelectorItem(Identity, DataSourceKind.Oracle)], false); } private sealed class DelayedThemeService : IThemeSelectionService { private int _callCount; public TaskCompletionSource FirstRequestStarted { get; } = new( TaskCreationOptions.RunContinuationsAsynchronously); public TaskCompletionSource ReleaseFirstRequest { get; } = new( TaskCreationOptions.RunContinuationsAsynchronously); public int CallCount => Volatile.Read(ref _callCount); public async Task SearchAsync( string query, ThemeSession nxtSession, int maximumResults = LegacyThemeSelectionService.DefaultMaximumResults, CancellationToken cancellationToken = default) { if (Interlocked.Increment(ref _callCount) == 1) { FirstRequestStarted.TrySetResult(); await ReleaseFirstRequest.Task.WaitAsync(cancellationToken); } return CountingThemeService.CreateThemeResult(query, nxtSession); } public Task PreviewAsync( ThemeSelectionIdentity identity, int maximumItems = LegacyThemeSelectionService.DefaultMaximumPreviewItems, CancellationToken cancellationToken = default) => Task.FromResult( new ThemePreviewResult(identity, DateTimeOffset.UtcNow, [], false)); } private sealed class CountingExpertService : IExpertSelectionService { private static readonly ExpertSelectionIdentity Identity = new("0001", "Expert A"); public List Queries { get; } = []; public Task SearchAsync( string query = "", int maximumResults = LegacyExpertSelectionService.DefaultMaximumResults, CancellationToken cancellationToken = default) { Queries.Add(query); return Task.FromResult(new ExpertSearchResult( query, DateTimeOffset.UtcNow, [new ExpertSelectorItem(Identity, DataSourceKind.Oracle)], false)); } public Task PreviewAsync( ExpertSelectionIdentity identity, int maximumItems = LegacyExpertSelectionService.DefaultMaximumPreviewItems, CancellationToken cancellationToken = default) => Task.FromResult( new ExpertPreviewResult(identity, DateTimeOffset.UtcNow, [], false)); } private sealed class CountingIndustryService : IIndustrySelectionService { public List Markets { get; } = []; public Task> GetAsync( IndustryMarket market, CancellationToken cancellationToken = default) { Markets.Add(market); return Task.FromResult>( [ new(market, "Industry 1", "001"), new(market, "Industry 2", "002") ]); } } private sealed class StaticComparisonDataService : ILegacyComparisonDataService { private static readonly IReadOnlyList Domestic = [ LegacyComparisonTarget.CreateKrxStock( LegacyComparisonDomesticMarket.Kospi, "Samsung Electronics", "005930"), LegacyComparisonTarget.CreateKrxStock( LegacyComparisonDomesticMarket.Kospi, "SK Hynix", "000660") ]; public Task> SearchDomesticAsync( string query, int maximumResults, CancellationToken cancellationToken = default) => Task.FromResult(Domestic); public Task> SearchWorldAsync( string query, int maximumResults, CancellationToken cancellationToken = default) => Task.FromResult< IReadOnlyList>([]); public Task ResolveStoredTargetAsync( LegacyComparisonStoredTargetReference target, CancellationToken cancellationToken = default) => Task.FromResult< LegacyComparisonTarget?>(null); } private sealed class CountingComparisonPairStore : ILegacyComparisonPairStore { public int ReadCount { get; private set; } public Task ReadCp949Async(CancellationToken cancellationToken = default) { ReadCount++; return Task.FromResult(null); } public Task WriteCp949Async( IReadOnlyList rows, CancellationToken cancellationToken = default) => Task.CompletedTask; } private sealed class DelayedComparisonPairStore : ILegacyComparisonPairStore { public TaskCompletionSource FirstReadStarted { get; } = new( TaskCreationOptions.RunContinuationsAsynchronously); public TaskCompletionSource ReleaseFirstRead { get; } = new( TaskCreationOptions.RunContinuationsAsynchronously); public int ReadCount { get; private set; } public async Task ReadCp949Async( CancellationToken cancellationToken = default) { ReadCount++; if (ReadCount == 1) { FirstReadStarted.TrySetResult(); await ReleaseFirstRead.Task.WaitAsync(cancellationToken); } return null; } public Task WriteCp949Async( IReadOnlyList rows, CancellationToken cancellationToken = default) => Task.CompletedTask; } private sealed class StaticOverseasIndustryService : IOverseasIndustryIndexSearchService { public Task SearchAsync( string query, int maximumResults = LegacyOverseasIndustryIndexSearchService.DefaultMaximumResults, CancellationToken cancellationToken = default) => Task.FromResult( new OverseasIndustryIndexSearchResult( query.Trim(), DateTimeOffset.UtcNow, [new OverseasIndustryIndexItem("Semiconductor", "US Semiconductor", "NAS@SOX")], false)); } private sealed class StaticOverseasStockService : IOverseasStockSearchService { public Task SearchAsync( string query, int maximumResults = LegacyOverseasStockSearchService.DefaultMaximumResults, CancellationToken cancellationToken = default) => Task.FromResult( new WorldStockSearchResult( query.Trim(), DateTimeOffset.UtcNow, [new WorldStockSearchItem("Apple", "NAS@AAPL", "US")], false)); } private sealed class DelayedOverseasIndustryService : IOverseasIndustryIndexSearchService { public TaskCompletionSource RequestStarted { get; } = new( TaskCreationOptions.RunContinuationsAsynchronously); public TaskCompletionSource ReleaseRequest { get; } = new( TaskCreationOptions.RunContinuationsAsynchronously); public async Task SearchAsync( string query, int maximumResults = LegacyOverseasIndustryIndexSearchService.DefaultMaximumResults, CancellationToken cancellationToken = default) { RequestStarted.TrySetResult(); await ReleaseRequest.Task.WaitAsync(cancellationToken); return new OverseasIndustryIndexSearchResult( query.Trim(), DateTimeOffset.UtcNow, [new OverseasIndustryIndexItem("Old", "Old result", "NAS@OLD")], false); } } private sealed class StaticTradingHaltService : ITradingHaltSelectionService { public Task SearchAsync( string query = "", int maximumResults = LegacyTradingHaltSelectionService.DefaultMaximumResults, CancellationToken cancellationToken = default) => Task.FromResult( new TradingHaltSelectionResult( query.Trim(), DateTimeOffset.UtcNow, [new TradingHaltSelection( TradingHaltMarket.Kospi, "005930", "Samsung Electronics")], false)); } }