using Microsoft.Data.SqlClient;
using ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Notifications;
///
/// Direct-SQL seeding helper for the Notification Report page Playwright E2E tests
/// (Notification Outbox #21).
///
///
/// The Notification Report page reads the central Notifications table through the
/// NotificationOutboxActor singleton. Its query path
/// (NotificationOutboxQueryRequest → NotificationOutboxRepository.QueryAsync)
/// is a pure read-from-table projection with NO default time window — a row INSERTed
/// directly into Notifications surfaces on the page exactly as a site-ingested row
/// would. The actor's manual Retry / Discard handlers
/// (RetryNotificationRequest / DiscardNotificationRequest) likewise act
/// purely on the central row (load by id, flip Status, persist) — there is no
/// site relay on this path — so a directly-seeded Parked row is genuinely
/// retryable/discardable from central. This mirrors :
/// each test inserts its own row at setup and best-effort deletes it at teardown, keeping
/// the suite self-contained without touching infra/mssql/seed-config.sql.
///
///
///
/// Rows are tagged with a unique ListName marker derived from the test name + a GUID
/// so the teardown DELETE never touches rows the cluster itself produced.
/// CreatedAt/SiteEnqueuedAt are pinned to "now" so the page's default
/// (unconstrained) query window sees the row.
///
///
internal static class NotificationDataSeeder
{
///
/// Connection string for the running cluster's configuration DB.
/// Delegates to .
///
public static string ConnectionString => PlaywrightDbConnection.ConnectionString;
///
/// Inserts one notification row with explicit and
/// (for stuck/age edge-case tests).
///
///
/// Populates every NOT NULL column (NotificationId, Type, ListName, Subject, Body,
/// Status, RetryCount, SourceSiteId, SiteEnqueuedAt, CreatedAt) and stamps the row
/// with the unique so the test can filter to it and
/// the teardown can delete by it. SiteEnqueuedAt is set equal to
/// . All nullable provenance columns (SourceNode,
/// OriginExecutionId, …) are left to default to NULL, which the page renders as an
/// em-dash.
///
///
///
/// To seed a genuinely stuck row set to Retrying (or
/// Pending) and to more than 10 minutes in the past —
/// IsStuck is derived as Status ∈ {Pending, Retrying} && CreatedAt <
/// now − 10 min.
///
///
/// GUID primary key (stored as its 36-char string form).
/// Unique per-run marker stored in ListName.
/// Subject text (searchable via the page's subject keyword box).
///
/// Status value stored as varchar (HasConversion<string>()): e.g. Pending,
/// Retrying, Parked, Delivered, Discarded.
///
///
/// Timestamp written to both CreatedAt and SiteEnqueuedAt. Pass a
/// back-dated value (e.g. DateTimeOffset.UtcNow.AddMinutes(-15)) to produce a
/// stuck row.
///
/// Originating site identifier (e.g. site-a).
/// Retry count to display on the row.
/// Optional last-error text shown beneath the subject.
/// Cancellation token.
public static async Task InsertNotificationAsync(
Guid notificationId,
string listNameMarker,
string subject,
string status,
DateTimeOffset createdAt,
string sourceSite = "site-a",
int retryCount = 0,
string? lastError = "SMTP 451 transient failure (seeded)",
CancellationToken ct = default)
{
// NotificationId is the varchar(64) primary key; Type/Status are stored as
// varchar(32) (HasConversion()). All NOT NULL columns are supplied;
// the nullable provenance columns (SourceNode, OriginExecutionId, …) are left
// to default to NULL, which the page renders as an em-dash.
const string sql = @"
INSERT INTO [Notifications]
([NotificationId], [Type], [ListName], [Subject], [Body], [Status], [RetryCount],
[LastError], [SourceSiteId], [SiteEnqueuedAt], [CreatedAt])
VALUES
(@id, @type, @listName, @subject, @body, @status, @retryCount,
@lastError, @sourceSite, @siteEnqueuedAt, @createdAt);";
await using var connection = new SqlConnection(ConnectionString);
await connection.OpenAsync(ct);
await using var cmd = connection.CreateCommand();
cmd.CommandText = sql;
cmd.Parameters.AddWithValue("@id", notificationId.ToString());
cmd.Parameters.AddWithValue("@type", "Email");
cmd.Parameters.AddWithValue("@listName", listNameMarker);
cmd.Parameters.AddWithValue("@subject", subject);
cmd.Parameters.AddWithValue("@body", "Seeded notification body for Playwright E2E.");
cmd.Parameters.AddWithValue("@status", status);
cmd.Parameters.AddWithValue("@retryCount", retryCount);
cmd.Parameters.AddWithValue("@lastError", (object?)lastError ?? DBNull.Value);
cmd.Parameters.AddWithValue("@sourceSite", sourceSite);
cmd.Parameters.AddWithValue("@siteEnqueuedAt", createdAt);
cmd.Parameters.AddWithValue("@createdAt", createdAt);
await cmd.ExecuteNonQueryAsync(ct);
}
///
/// Inserts a single Parked row into the central Notifications table.
/// Populates every NOT NULL column (NotificationId, Type, ListName, Subject, Body,
/// Status, RetryCount, SourceSiteId, SiteEnqueuedAt, CreatedAt) and stamps the row with
/// the unique so the test can filter to it and the
/// teardown can delete by it. Status is fixed to Parked — the only status
/// from which the page exposes Retry/Discard. Timestamps are pinned to "now" so the
/// page's default unconstrained query window sees the row.
///
/// GUID primary key (stored as its 36-char string form).
/// Unique per-run marker stored in ListName.
/// Subject text (searchable via the page's subject keyword box).
/// Originating site identifier (e.g. site-a).
/// Retry count to display on the row.
/// Optional last-error text shown beneath the subject.
/// Cancellation token.
public static Task InsertParkedNotificationAsync(
Guid notificationId,
string listNameMarker,
string subject,
string sourceSite,
int retryCount = 3,
string? lastError = "SMTP 451 transient failure (seeded)",
CancellationToken ct = default)
=> InsertNotificationAsync(
notificationId, listNameMarker, subject,
status: "Parked", createdAt: DateTimeOffset.UtcNow,
sourceSite: sourceSite, retryCount: retryCount, lastError: lastError, ct: ct);
///
/// Best-effort cleanup. Deletes every Notifications row whose ListName
/// equals . Swallows all errors — the marker carries a
/// per-run GUID so the rows are unique to this test run. A Retry that flips the row back
/// to Pending (and a subsequent dispatch sweep) does not change the ListName,
/// so the marker still matches whatever terminal state the row ends in.
///
public static async Task DeleteByMarkerAsync(string listNameMarker, CancellationToken ct = default)
{
try
{
await using var connection = new SqlConnection(ConnectionString);
await connection.OpenAsync(ct);
await using var cmd = connection.CreateCommand();
cmd.CommandText = "DELETE FROM [Notifications] WHERE [ListName] = @listName";
cmd.Parameters.AddWithValue("@listName", listNameMarker);
await cmd.ExecuteNonQueryAsync(ct);
}
catch
{
// Best-effort — the marker carries a GUID so the rows are unique to this test
// run and won't collide on the next pass.
}
}
///
/// Probe whether the configuration DB is reachable. Tests gate their per-test setup on
/// this so a downed cluster surfaces a clear message rather than an opaque
/// .
///
public static async Task IsAvailableAsync(CancellationToken ct = default)
{
try
{
await using var connection = new SqlConnection(ConnectionString);
await connection.OpenAsync(ct);
return true;
}
catch
{
return false;
}
}
}