using System.Text.Json; using MBN_STOCK_WEBVIEW.LegacyApplication; using MMoneyCoderSharp.Data; namespace MBN_STOCK_WEBVIEW.LegacyBridge.Tests; public sealed class LegacyManualFinancialBridgeTests { [Theory] [InlineData("revenue-composition", ManualFinancialScreenKind.RevenueComposition)] [InlineData("growth-metrics", ManualFinancialScreenKind.GrowthMetrics)] [InlineData("sales", ManualFinancialScreenKind.Sales)] [InlineData("operating-profit", ManualFinancialScreenKind.OperatingProfit)] public void Open_uses_closed_GraphE_screen_identity( string screen, ManualFinancialScreenKind expected) { var json = JsonSerializer.Serialize(new { type = "open-manual-financial", payload = new { screen } }); Assert.True(LegacyBridgeProtocol.TryParseIntent(json, out var parsed, out _)); Assert.Equal(expected, Assert.IsType(parsed).Screen); Assert.False(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"open-manual-financial\",\"payload\":{\"screen\":\"INPUT_SELL\"}}", out _, out _)); } [Fact] public void Save_parses_all_typed_record_shapes_without_accepting_DB_authority() { AssertSaveType( """ {"screen":"revenue-composition","stockName":"Alpha","baseDate":"2026.07","slices":[{"label":"A","percentageText":"25"},null,null,null,null]} """); AssertSaveType( """ {"screen":"growth-metrics","stockName":"Alpha","salesGrowth":[1,2,null,4],"operatingProfitGrowth":[5,6,7,8],"netAssetGrowth":[9,10,11,12],"netIncomeGrowth":[13,14,15,16],"periods":["1Q","2Q","3Q","4Q"]} """); AssertSaveType( """ {"screen":"sales","stockName":"Alpha","quarters":[{"quarter":"1Q","value":1},{"quarter":"2Q","value":2},{"quarter":"3Q","value":3},{"quarter":"4Q","value":4},{"quarter":"5Q","value":5},{"quarter":"6Q","value":6}]} """); AssertSaveType( """ {"screen":"operating-profit","stockName":"Alpha","quarters":[{"quarter":"1Q","value":1},{"quarter":"2Q","value":2},{"quarter":"3Q","value":3},{"quarter":"4Q","value":4},{"quarter":"5Q","value":5},{"quarter":"6Q","value":6}]} """); Assert.False(LegacyBridgeProtocol.TryParseIntent( """ {"type":"save-manual-financial","payload":{"record":{"screen":"sales","stockName":"Alpha","rowVersion":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","quarters":[]}}} """, out _, out _)); } [Fact] public void CRUD_and_action_intents_accept_only_opaque_ids_or_empty_payloads() { Assert.True(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"load-manual-financial\",\"payload\":{\"resultId\":\"mf-row-0123456789abcdef\"}}", out var loaded, out _)); Assert.IsType(loaded); foreach (var type in new[] { "begin-manual-financial-create", "delete-manual-financial", "delete-all-manual-financial", "close-manual-financial" }) { Assert.True(LegacyBridgeProtocol.TryParseIntent( $"{{\"type\":\"{type}\",\"payload\":{{}}}}", out _, out _)); Assert.False(LegacyBridgeProtocol.TryParseIntent( $"{{\"type\":\"{type}\",\"payload\":{{\"table\":\"INPUT_SELL\"}}}}", out _, out _)); } Assert.True(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"activate-manual-financial-action\",\"payload\":{\"actionId\":\"sales\"}}", out var action, out _)); Assert.Equal( "sales", Assert.IsType(action).ActionId); Assert.False(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"activate-manual-financial-action\",\"payload\":{\"actionId\":\"s5080\"}}", out _, out _)); } [Fact] public void GraphE_find_and_name_sort_require_strict_generation_bound_payloads() { Assert.True(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"find-manual-financial\",\"payload\":{\"query\":\"Alpha\",\"direction\":\"down\",\"generation\":7}}", out var find, out _)); var parsedFind = Assert.IsType(find); Assert.Equal(LegacyManualFinancialFindDirection.Down, parsedFind.Direction); Assert.Equal(7, parsedFind.Generation); Assert.True(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"toggle-manual-financial-name-sort\",\"payload\":{\"generation\":7}}", out var sort, out _)); Assert.Equal(7, Assert.IsType(sort).Generation); Assert.False(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"find-manual-financial\",\"payload\":{\"query\":\"Alpha\",\"direction\":\"sideways\",\"generation\":7}}", out _, out _)); Assert.False(LegacyBridgeProtocol.TryParseIntent( "{\"type\":\"toggle-manual-financial-name-sort\",\"payload\":{\"generation\":7,\"table\":\"INPUT_SELL\"}}", out _, out _)); } [Fact] public async Task Projection_exposes_editor_values_but_not_rowVersion_or_DB_scene_authority() { var manualService = new StaticManualService(); var workflow = new LegacyManualFinancialWorkflow( manualService, new StaticStockService()); var controller = new LegacyOperatorController( new EmptyStockLookup(), manualFinancialWorkflow: workflow); var opened = await controller.OpenManualFinancialAsync(ManualFinancialScreenKind.Sales); var loaded = await controller.LoadManualFinancialAsync( Assert.Single(opened.ManualFinancial!.Rows).ResultId); using var document = JsonDocument.Parse(LegacyBridgeProtocol.SerializeState(loaded)); var manual = document.RootElement.GetProperty("payload").GetProperty("manualFinancial"); Assert.Equal("sales", manual.GetProperty("screen").GetString()); Assert.True(manual.GetProperty("generation").GetInt64() > 0); Assert.Equal("ascending", manual.GetProperty("nameSortDirection").GetString()); Assert.Equal("Alpha", manual.GetProperty("selectedRecord").GetProperty("stockName").GetString()); Assert.Equal("sales", manual.GetProperty("action").GetProperty("actionId").GetString()); Assert.False(manual.TryGetProperty("rowVersion", out _)); Assert.False(manual.TryGetProperty("tableName", out _)); Assert.False(manual.TryGetProperty("builderKey", out _)); Assert.False(manual.GetProperty("stockCandidates")[0].TryGetProperty("code", out _)); } private static void AssertSaveType(string recordJson) where TRecord : ManualFinancialRecord { var json = $"{{\"type\":\"save-manual-financial\",\"payload\":{{\"record\":{recordJson}}}}}"; Assert.True(LegacyBridgeProtocol.TryParseIntent(json, out var parsed, out _)); Assert.IsType(Assert.IsType(parsed).Record); } private sealed class EmptyStockLookup : ILegacyStockLookup { public Task SearchAsync( string query, CancellationToken cancellationToken = default) => Task.FromResult(new LegacyStockSearchData(query, [], [])); } private sealed class StaticStockService : IStockSearchService { public Task SearchAsync( string query, int maximumResults = LegacyStockSearchService.DefaultMaximumResults, CancellationToken cancellationToken = default) => Task.FromResult(new StockSearchResult( query, DateTimeOffset.UtcNow, [new StockSearchItem( StockMarket.Kospi, DataSourceKind.Oracle, "Alpha", "000001")], false)); } private sealed class StaticManualService : IManualFinancialScreenService { private static readonly ManualFinancialSnapshot Row = new( new ManualSalesRecord( new ManualFinancialIdentity(ManualFinancialScreenKind.Sales, "Alpha"), Enumerable.Range(1, 6) .Select(index => new ManualQuarterValue($"{index}Q", index)) .ToArray()), new string('A', 64)); public bool CanMutate => false; public Task SearchAsync( ManualFinancialScreenKind screen, string query = "", int maximumResults = LegacyManualFinancialScreenService.DefaultMaximumResults, CancellationToken cancellationToken = default) => Task.FromResult(new ManualFinancialSearchResult( screen, query, DateTimeOffset.UtcNow, screen == ManualFinancialScreenKind.Sales ? [Row] : [], false)); public Task GetAsync( ManualFinancialIdentity identity, CancellationToken cancellationToken = default) => Task.FromResult(Row); public Task CreateAsync( ManualFinancialRecord record, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task UpdateAsync( ManualFinancialSnapshot expected, ManualFinancialRecord replacement, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task DeleteAsync( ManualFinancialSnapshot expected, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task DeleteAllAsync( ManualFinancialScreenKind screen, CancellationToken cancellationToken = default) => throw new NotSupportedException(); } }