using Microsoft.Data.SqlClient;
using ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Cluster;

namespace ZB.MOM.WW.ScadaBridge.CentralUI.PlaywrightTests.Notifications;

/// <summary>
/// Direct-SQL seeding helper for the Notification Report page Playwright E2E tests
/// (Notification Outbox #21).
///
/// <para>
/// The Notification Report page reads the central <c>Notifications</c> table through the
/// <c>NotificationOutboxActor</c> singleton. Its query path
/// (<c>NotificationOutboxQueryRequest</c> → <c>NotificationOutboxRepository.QueryAsync</c>)
/// is a pure read-from-table projection with NO default time window — a row INSERTed
/// directly into <c>Notifications</c> surfaces on the page exactly as a site-ingested row
/// would. The actor's manual Retry / Discard handlers
/// (<c>RetryNotificationRequest</c> / <c>DiscardNotificationRequest</c>) likewise act
/// purely on the central row (load by id, flip <c>Status</c>, persist) — there is no
/// site relay on this path — so a directly-seeded <c>Parked</c> row is genuinely
/// retryable/discardable from central. This mirrors <see cref="SiteCalls.SiteCallDataSeeder"/>:
/// each test inserts its own row at setup and best-effort deletes it at teardown, keeping
/// the suite self-contained without touching <c>infra/mssql/seed-config.sql</c>.
/// </para>
///
/// <para>
/// Rows are tagged with a unique <c>ListName</c> marker derived from the test name + a GUID
/// so the teardown <c>DELETE</c> never touches rows the cluster itself produced.
/// <c>CreatedAt</c>/<c>SiteEnqueuedAt</c> are pinned to "now" so the page's default
/// (unconstrained) query window sees the row.
/// </para>
/// </summary>
internal static class NotificationDataSeeder
{
    /// <summary>
    /// Connection string for the running cluster's configuration DB.
    /// Delegates to <see cref="PlaywrightDbConnection.ConnectionString"/>.
    /// </summary>
    public static string ConnectionString => PlaywrightDbConnection.ConnectionString;

    /// <summary>
    /// Inserts one notification row with explicit <paramref name="status"/> and
    /// <paramref name="createdAt"/> (for stuck/age edge-case tests).
    ///
    /// <para>
    /// Populates every NOT NULL column (NotificationId, Type, ListName, Subject, Body,
    /// Status, RetryCount, SourceSiteId, SiteEnqueuedAt, CreatedAt) and stamps the row
    /// with the unique <paramref name="listNameMarker"/> so the test can filter to it and
    /// the teardown can delete by it. <c>SiteEnqueuedAt</c> is set equal to
    /// <paramref name="createdAt"/>. All nullable provenance columns (SourceNode,
    /// OriginExecutionId, …) are left to default to NULL, which the page renders as an
    /// em-dash.
    /// </para>
    ///
    /// <para>
    /// To seed a genuinely stuck row set <paramref name="status"/> to <c>Retrying</c> (or
    /// <c>Pending</c>) and <paramref name="createdAt"/> to more than 10 minutes in the past —
    /// <c>IsStuck</c> is derived as <c>Status ∈ {Pending, Retrying} &amp;&amp; CreatedAt &lt;
    /// now − 10 min</c>.
    /// </para>
    /// </summary>
    /// <param name="notificationId">GUID primary key (stored as its 36-char string form).</param>
    /// <param name="listNameMarker">Unique per-run marker stored in <c>ListName</c>.</param>
    /// <param name="subject">Subject text (searchable via the page's subject keyword box).</param>
    /// <param name="status">
    /// Status value stored as varchar (HasConversion&lt;string&gt;()): e.g. <c>Pending</c>,
    /// <c>Retrying</c>, <c>Parked</c>, <c>Delivered</c>, <c>Discarded</c>.
    /// </param>
    /// <param name="createdAt">
    /// Timestamp written to both <c>CreatedAt</c> and <c>SiteEnqueuedAt</c>. Pass a
    /// back-dated value (e.g. <c>DateTimeOffset.UtcNow.AddMinutes(-15)</c>) to produce a
    /// stuck row.
    /// </param>
    /// <param name="sourceSite">Originating site identifier (e.g. <c>site-a</c>).</param>
    /// <param name="retryCount">Retry count to display on the row.</param>
    /// <param name="lastError">Optional last-error text shown beneath the subject.</param>
    /// <param name="ct">Cancellation token.</param>
    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<string>()). 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);
    }

    /// <summary>
    /// Inserts a single <c>Parked</c> row into the central <c>Notifications</c> table.
    /// Populates every NOT NULL column (NotificationId, Type, ListName, Subject, Body,
    /// Status, RetryCount, SourceSiteId, SiteEnqueuedAt, CreatedAt) and stamps the row with
    /// the unique <paramref name="listNameMarker"/> so the test can filter to it and the
    /// teardown can delete by it. <c>Status</c> is fixed to <c>Parked</c> — 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.
    /// </summary>
    /// <param name="notificationId">GUID primary key (stored as its 36-char string form).</param>
    /// <param name="listNameMarker">Unique per-run marker stored in <c>ListName</c>.</param>
    /// <param name="subject">Subject text (searchable via the page's subject keyword box).</param>
    /// <param name="sourceSite">Originating site identifier (e.g. <c>site-a</c>).</param>
    /// <param name="retryCount">Retry count to display on the row.</param>
    /// <param name="lastError">Optional last-error text shown beneath the subject.</param>
    /// <param name="ct">Cancellation token.</param>
    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);

    /// <summary>
    /// Best-effort cleanup. Deletes every <c>Notifications</c> row whose <c>ListName</c>
    /// equals <paramref name="listNameMarker"/>. 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 <c>Pending</c> (and a subsequent dispatch sweep) does not change the <c>ListName</c>,
    /// so the marker still matches whatever terminal state the row ends in.
    /// </summary>
    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.
        }
    }

    /// <summary>
    /// 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
    /// <see cref="SqlException"/>.
    /// </summary>
    public static async Task<bool> IsAvailableAsync(CancellationToken ct = default)
    {
        try
        {
            await using var connection = new SqlConnection(ConnectionString);
            await connection.OpenAsync(ct);
            return true;
        }
        catch
        {
            return false;
        }
    }
}
