using Microsoft.Data.SqlClient;
namespace ScadaLink.CentralUI.PlaywrightTests.SiteCalls;
///
/// Direct-SQL seeding helper for the Site Calls page Playwright E2E tests
/// (Site Call Audit #22, follow-ups Task 6).
///
///
/// The Site Calls page reads the central SiteCalls table through the
/// SiteCallAuditActor, which is a pure read-from-table mirror — so a row
/// INSERTed directly into SiteCalls surfaces on the page exactly as a
/// telemetry-ingested row would. Mirrors :
/// each test inserts its own rows at setup and best-effort deletes them at
/// teardown, keeping the suite self-contained without touching
/// infra/mssql/seed-config.sql.
///
///
///
/// Rows are tagged with a unique Target prefix derived from the test name
/// + a GUID so the teardown DELETE never touches rows the cluster itself
/// produced. CreatedAtUtc/UpdatedAtUtc are pinned to "now" so the
/// page's default (unconstrained) query window sees the row.
///
///
internal static class SiteCallDataSeeder
{
private const string DefaultConnectionString =
"Server=localhost,1433;Database=ScadaLinkConfig;User Id=scadalink_app;Password=ScadaLink_Dev1#;TrustServerCertificate=true;Encrypt=false;Connect Timeout=5";
private const string EnvVar = "SCADALINK_PLAYWRIGHT_DB";
///
/// Connection string for the running cluster's configuration DB. Resolved
/// from SCADALINK_PLAYWRIGHT_DB when set, otherwise the local docker
/// dev defaults.
///
public static string ConnectionString
{
get
{
var fromEnv = Environment.GetEnvironmentVariable(EnvVar);
return string.IsNullOrWhiteSpace(fromEnv) ? DefaultConnectionString : fromEnv;
}
}
///
/// Inserts a single row into the central SiteCalls table. Optional
/// fields are nullable so a test can shape the row to the status/channel it
/// needs for its grid assertions. TrackedOperationId is stored as the
/// 36-character GUID string the entity mapping expects.
///
public static async Task InsertSiteCallAsync(
Guid trackedOperationId,
string channel,
string target,
string sourceSite,
string status,
int retryCount,
DateTime createdAtUtc,
DateTime updatedAtUtc,
string? lastError = null,
int? httpStatus = null,
DateTime? terminalAtUtc = null,
CancellationToken ct = default)
{
const string sql = @"
INSERT INTO [SiteCalls]
([TrackedOperationId], [Channel], [Target], [SourceSite], [Status], [RetryCount],
[LastError], [HttpStatus], [CreatedAtUtc], [UpdatedAtUtc], [TerminalAtUtc], [IngestedAtUtc])
VALUES
(@id, @channel, @target, @sourceSite, @status, @retryCount,
@lastError, @httpStatus, @createdAtUtc, @updatedAtUtc, @terminalAtUtc, SYSUTCDATETIME());";
await using var connection = new SqlConnection(ConnectionString);
await connection.OpenAsync(ct);
await using var cmd = connection.CreateCommand();
cmd.CommandText = sql;
cmd.Parameters.AddWithValue("@id", trackedOperationId.ToString());
cmd.Parameters.AddWithValue("@channel", channel);
cmd.Parameters.AddWithValue("@target", target);
cmd.Parameters.AddWithValue("@sourceSite", sourceSite);
cmd.Parameters.AddWithValue("@status", status);
cmd.Parameters.AddWithValue("@retryCount", retryCount);
cmd.Parameters.AddWithValue("@lastError", (object?)lastError ?? DBNull.Value);
cmd.Parameters.AddWithValue("@httpStatus", (object?)httpStatus ?? DBNull.Value);
cmd.Parameters.AddWithValue("@createdAtUtc", createdAtUtc);
cmd.Parameters.AddWithValue("@updatedAtUtc", updatedAtUtc);
cmd.Parameters.AddWithValue("@terminalAtUtc", (object?)terminalAtUtc ?? DBNull.Value);
await cmd.ExecuteNonQueryAsync(ct);
}
///
/// Best-effort cleanup. Deletes every SiteCalls row whose Target
/// starts with . Swallows all errors — the
/// prefix carries a per-run GUID so the rows are unique to this test run.
///
public static async Task DeleteByTargetPrefixAsync(string targetPrefix, 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 [SiteCalls] WHERE [Target] LIKE @prefix";
cmd.Parameters.AddWithValue("@prefix", targetPrefix + "%");
await cmd.ExecuteNonQueryAsync(ct);
}
catch
{
// Best-effort — the prefix 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;
}
}
}