#nullable enable using MBN_STOCK_WEBVIEW.Core.Playout.Scenes; using MBN_STOCK_WEBVIEW.Infrastructure; using MMoneyCoderSharp.Data; namespace MBN_STOCK_WEBVIEW.DbWriteSmoke; internal static class ProductionDevelopmentDatabaseSmokePlan { internal static IReadOnlyList Create( string? configurationPath) { var options = new DatabaseOptionsLoader().Load(configurationPath); // This executable never inherits the production read retry count. A // timeout is surfaced once so the harness cannot conceal an uncertain // state transition behind another attempt. options.Resilience.MaximumRetryCount = 0; options.Resilience.InitialRetryDelayMilliseconds = 0; var runtime = DatabaseRuntime.Create(options); var manualMutation = new OracleManualFinancialMutationExecutor( runtime.ConnectionFactory, options.Resilience); var namedMutation = new OracleNamedPlaylistMutationExecutor( runtime.ConnectionFactory, options.Resilience); var catalogMutation = new OperatorCatalogMutationExecutor( runtime.ConnectionFactory, options.Resilience); var manual = new LegacyManualFinancialScreenService( runtime.Executor, manualMutation); var named = new LegacyNamedPlaylistPersistenceService( runtime.Executor, namedMutation); var themePersistence = new LegacyThemeCatalogPersistenceService( runtime.Executor, catalogMutation); var themeSelection = new LegacyThemeSelectionService(runtime.Executor); var expertPersistence = new LegacyExpertCatalogPersistenceService( runtime.Executor, catalogMutation); var expertSelection = new LegacyExpertSelectionService(runtime.Executor); var childGuard = new ExistingChildRowGuard(runtime.Executor); var run = SmokeRunIdentity.Create(DateTimeOffset.UtcNow, Guid.NewGuid()); return [ CreateManualScenario( manual, CreateManualRecord(ManualFinancialScreenKind.RevenueComposition, run.For("G_PIE"))), CreateManualScenario( manual, CreateManualRecord(ManualFinancialScreenKind.GrowthMetrics, run.For("G_GROW"))), CreateManualScenario( manual, CreateManualRecord(ManualFinancialScreenKind.Sales, run.For("G_SELL"))), CreateManualScenario( manual, CreateManualRecord(ManualFinancialScreenKind.OperatingProfit, run.For("G_PROFIT"))), CreateNamedPlaylistScenario(named, childGuard, run.For("PLAYLIST")), CreateThemeScenario( themePersistence, themeSelection, childGuard, ThemeMarket.Krx, run.For("THEME_KRX")), CreateThemeScenario( themePersistence, themeSelection, childGuard, ThemeMarket.Nxt, run.For("THEME_NXT")), CreateExpertScenario( expertPersistence, expertSelection, childGuard, run.For("EXPERT")) ]; } private static DevelopmentDatabaseSmokeScenario CreateManualScenario( IManualFinancialScreenService service, ManualFinancialRecord expectedRecord) { var identity = expectedRecord.Identity; ManualFinancialSnapshot? freshSnapshot = null; return new DevelopmentDatabaseSmokeScenario( "GraphE/" + identity.Screen, async cancellationToken => { var search = await service.SearchAsync( identity.Screen, identity.StockName, 10, cancellationToken).ConfigureAwait(false); if (search.IsTruncated || search.Items.Any(item => string.Equals( item.Record.Identity.StockName, identity.StockName, StringComparison.Ordinal))) { throw Collision("GraphE preflight did not prove absence."); } }, cancellationToken => IgnoreResultAsync( service.CreateAsync(expectedRecord, cancellationToken)), async cancellationToken => { freshSnapshot = await service.GetAsync(identity, cancellationToken) .ConfigureAwait(false); VerifyManualRecord(expectedRecord, freshSnapshot); }, save: null, verifySave: null, async cancellationToken => { freshSnapshot ??= await service.GetAsync(identity, cancellationToken) .ConfigureAwait(false); await service.DeleteAsync(freshSnapshot, cancellationToken).ConfigureAwait(false); }, cancellationToken => EnsureManualAbsentAsync(service, identity, cancellationToken), IsOutcomeUnknown); } private static DevelopmentDatabaseSmokeScenario CreateNamedPlaylistScenario( INamedPlaylistPersistenceService service, ExistingChildRowGuard childGuard, string title) { string? programCode = null; var savedItem = new NamedPlaylistStoredItem( 0, true, new LegacySceneSelection( "KOSPI", "SMOKE", "PLATE", "CURRENT", string.Empty), new NamedPlaylistPageState(1, 1)); return new DevelopmentDatabaseSmokeScenario( "AList/PList named playlist", async cancellationToken => { programCode = await service.SuggestNextProgramCodeAsync(cancellationToken) .ConfigureAwait(false); var list = await service.ListAsync( LegacyNamedPlaylistPersistenceService.MaximumDefinitions, cancellationToken).ConfigureAwait(false); if (list.IsTruncated || list.Items.Any(item => string.Equals(item.Title, title, StringComparison.Ordinal))) { throw Collision("Named-playlist title absence was not proven."); } await EnsureNamedPlaylistAbsentAsync( service, programCode, cancellationToken).ConfigureAwait(false); await childGuard.EnsureNamedPlaylistChildrenAbsentAsync( programCode, cancellationToken).ConfigureAwait(false); }, cancellationToken => service.CreateDefinitionAsync( Required(programCode), title, cancellationToken), async cancellationToken => { var document = await service.LoadAsync( Required(programCode), cancellationToken).ConfigureAwait(false); if (!string.Equals( document.Definition.Title, title, StringComparison.Ordinal) || document.Items.Count != 0) { throw Verification("Named-playlist create readback did not match."); } }, cancellationToken => service.ReplaceItemsAsync( Required(programCode), [savedItem], cancellationToken), async cancellationToken => { var document = await service.LoadAsync( Required(programCode), cancellationToken).ConfigureAwait(false); if (!string.Equals( document.Definition.Title, title, StringComparison.Ordinal) || document.Items.Count != 1 || document.Items[0] != savedItem) { throw Verification("Named-playlist save readback did not match."); } }, cancellationToken => service.DeleteAsync( Required(programCode), cancellationToken), async cancellationToken => { await EnsureNamedPlaylistAbsentAsync( service, Required(programCode), cancellationToken).ConfigureAwait(false); var list = await service.ListAsync( LegacyNamedPlaylistPersistenceService.MaximumDefinitions, cancellationToken).ConfigureAwait(false); if (list.IsTruncated || list.Items.Any(item => string.Equals(item.Title, title, StringComparison.Ordinal))) { throw Verification("Named-playlist cleanup absence was not proven."); } }, IsOutcomeUnknown); } private static DevelopmentDatabaseSmokeScenario CreateThemeScenario( IThemeCatalogPersistenceService persistence, IThemeSelectionService selection, ExistingChildRowGuard childGuard, ThemeMarket market, string title) { string? themeCode = null; ThemeSelectionIdentity? freshIdentity = null; var session = market == ThemeMarket.Krx ? ThemeSession.NotApplicable : ThemeSession.PreMarket; return new DevelopmentDatabaseSmokeScenario( "ThemeA/" + market, async cancellationToken => { await EnsureThemeAbsentAsync(selection, title, cancellationToken) .ConfigureAwait(false); themeCode = await persistence.SuggestNextCodeAsync(market, cancellationToken) .ConfigureAwait(false); await childGuard.EnsureThemeChildrenAbsentAsync( market, themeCode, cancellationToken).ConfigureAwait(false); }, cancellationToken => IgnoreResultAsync( persistence.CreateAsync( new ThemeCatalogDefinition( market, Required(themeCode), title, []), cancellationToken)), async cancellationToken => { var search = await selection.SearchAsync( title, ThemeSession.PreMarket, LegacyThemeSelectionService.MaximumResults, cancellationToken).ConfigureAwait(false); if (search.IsTruncated) { throw Verification("Theme readback was truncated."); } var matches = search.Items.Where(item => string.Equals( item.Identity.ThemeTitle, title, StringComparison.Ordinal)).ToArray(); if (matches.Length != 1 || matches[0].Identity.Market != market || !string.Equals( matches[0].Identity.ThemeCode, themeCode, StringComparison.Ordinal)) { throw Verification("Theme create readback did not match."); } freshIdentity = matches[0].Identity with { Session = session }; var preview = await selection.PreviewAsync( freshIdentity, LegacyThemeSelectionService.MaximumPreviewItems, cancellationToken).ConfigureAwait(false); if (preview.Items.Count != 0 || preview.IsTruncated) { throw Verification("Theme preview readback did not match."); } }, save: null, verifySave: null, cancellationToken => IgnoreResultAsync( persistence.DeleteAsync( freshIdentity ?? new ThemeSelectionIdentity( market, session, Required(themeCode), title), cancellationToken)), cancellationToken => EnsureThemeAbsentAsync(selection, title, cancellationToken), IsOutcomeUnknown); } private static DevelopmentDatabaseSmokeScenario CreateExpertScenario( IExpertCatalogPersistenceService persistence, IExpertSelectionService selection, ExistingChildRowGuard childGuard, string name) { string? expertCode = null; ExpertSelectionIdentity? freshIdentity = null; return new DevelopmentDatabaseSmokeScenario( "EList expert catalog", async cancellationToken => { await EnsureExpertAbsentAsync(selection, name, cancellationToken) .ConfigureAwait(false); expertCode = await persistence.SuggestNextCodeAsync(cancellationToken) .ConfigureAwait(false); await childGuard.EnsureExpertChildrenAbsentAsync( expertCode, cancellationToken).ConfigureAwait(false); }, cancellationToken => IgnoreResultAsync( persistence.CreateAsync( new ExpertCatalogDefinition( new ExpertSelectionIdentity(Required(expertCode), name), []), cancellationToken)), async cancellationToken => { var search = await selection.SearchAsync( name, LegacyExpertSelectionService.MaximumResults, cancellationToken).ConfigureAwait(false); if (search.IsTruncated) { throw Verification("Expert readback was truncated."); } var matches = search.Items.Where(item => string.Equals( item.Identity.ExpertName, name, StringComparison.Ordinal)).ToArray(); if (matches.Length != 1 || !string.Equals( matches[0].Identity.ExpertCode, expertCode, StringComparison.Ordinal)) { throw Verification("Expert create readback did not match."); } freshIdentity = matches[0].Identity; var preview = await selection.PreviewAsync( freshIdentity, LegacyExpertSelectionService.MaximumPreviewItems, cancellationToken).ConfigureAwait(false); if (preview.Items.Count != 0 || preview.IsTruncated) { throw Verification("Expert preview readback did not match."); } }, save: null, verifySave: null, cancellationToken => IgnoreResultAsync( persistence.DeleteAsync( freshIdentity ?? new ExpertSelectionIdentity( Required(expertCode), name), cancellationToken)), cancellationToken => EnsureExpertAbsentAsync(selection, name, cancellationToken), IsOutcomeUnknown); } private static ManualFinancialRecord CreateManualRecord( ManualFinancialScreenKind screen, string stockName) { var identity = new ManualFinancialIdentity(screen, stockName); return screen switch { ManualFinancialScreenKind.RevenueComposition => new ManualRevenueCompositionRecord( identity, "2026-07", [new("SMOKE", "100", 100), null, null, null, null]), ManualFinancialScreenKind.GrowthMetrics => new ManualGrowthMetricsRecord( identity, new(1, 2, 3, 4), new(5, 6, 7, 8), new(9, 10, 11, 12), new(13, 14, 15, 16), new("P1", "P2", "P3", "P4")), ManualFinancialScreenKind.Sales => new ManualSalesRecord(identity, SixQuarters()), ManualFinancialScreenKind.OperatingProfit => new ManualOperatingProfitRecord(identity, SixQuarters()), _ => throw new ArgumentOutOfRangeException(nameof(screen)) }; } private static IReadOnlyList SixQuarters() => [ new("Q1", 10), new("Q2", -20), new("Q3", 30), new("Q4", 40), new("Q5", 50), new("Q6", 60) ]; private static void VerifyManualRecord( ManualFinancialRecord expected, ManualFinancialSnapshot actual) { if (string.IsNullOrWhiteSpace(actual.RowVersion) || actual.Record.Identity != expected.Identity) { throw Verification("GraphE identity or row version did not match."); } var matches = (expected, actual.Record) switch { (ManualRevenueCompositionRecord left, ManualRevenueCompositionRecord right) => string.Equals(left.BaseDate, right.BaseDate, StringComparison.Ordinal) && left.Slices.SequenceEqual(right.Slices), (ManualGrowthMetricsRecord left, ManualGrowthMetricsRecord right) => left.SalesGrowth == right.SalesGrowth && left.OperatingProfitGrowth == right.OperatingProfitGrowth && left.NetAssetGrowth == right.NetAssetGrowth && left.NetIncomeGrowth == right.NetIncomeGrowth && left.Periods == right.Periods, (ManualSalesRecord left, ManualSalesRecord right) => left.Quarters.SequenceEqual(right.Quarters), (ManualOperatingProfitRecord left, ManualOperatingProfitRecord right) => left.Quarters.SequenceEqual(right.Quarters), _ => false }; if (!matches) { throw Verification("GraphE create readback did not match."); } } private static async Task EnsureManualAbsentAsync( IManualFinancialScreenService service, ManualFinancialIdentity identity, CancellationToken cancellationToken) { try { _ = await service.GetAsync(identity, cancellationToken).ConfigureAwait(false); } catch (ManualFinancialNotFoundException) { return; } throw Verification("GraphE record still exists."); } private static async Task EnsureNamedPlaylistAbsentAsync( INamedPlaylistPersistenceService service, string programCode, CancellationToken cancellationToken) { try { _ = await service.LoadAsync(programCode, cancellationToken).ConfigureAwait(false); } catch (NamedPlaylistNotFoundException) { return; } throw Verification("Named playlist still exists."); } private static async Task EnsureThemeAbsentAsync( IThemeSelectionService selection, string title, CancellationToken cancellationToken) { var search = await selection.SearchAsync( title, ThemeSession.PreMarket, LegacyThemeSelectionService.MaximumResults, cancellationToken).ConfigureAwait(false); if (search.IsTruncated || search.Items.Any(item => string.Equals( item.Identity.ThemeTitle, title, StringComparison.Ordinal))) { throw Collision("Theme absence was not proven."); } } private static async Task EnsureExpertAbsentAsync( IExpertSelectionService selection, string name, CancellationToken cancellationToken) { var search = await selection.SearchAsync( name, LegacyExpertSelectionService.MaximumResults, cancellationToken).ConfigureAwait(false); if (search.IsTruncated || search.Items.Any(item => string.Equals( item.Identity.ExpertName, name, StringComparison.Ordinal))) { throw Collision("Expert absence was not proven."); } } private static async Task IgnoreResultAsync(Task operation) { _ = await operation.ConfigureAwait(false); } private static string Required(string? value) => value ?? throw new InvalidOperationException("Smoke preflight did not produce an identity."); private static bool IsOutcomeUnknown(Exception exception) { ArgumentNullException.ThrowIfNull(exception); if (exception is ManualFinancialMutationException manual && manual.OutcomeUnknown || exception is NamedPlaylistMutationException named && named.OutcomeUnknown || exception is OperatorCatalogMutationException catalog && catalog.OutcomeUnknown) { return true; } if (exception is AggregateException aggregate && aggregate.InnerExceptions.Any(IsOutcomeUnknown)) { return true; } return exception.InnerException is not null && IsOutcomeUnknown(exception.InnerException); } private static InvalidOperationException Collision(string message) => new(message); private static InvalidOperationException Verification(string message) => new(message); }