using System.Text.Json;
using GB5Shared.DTO.Framework.Login;
using GB5Shared.Resource.Response;
using Microsoft.Extensions.Logging;
using TCMSBLL.Interfaces;
using TCMSDAL.DTO;
using TCMSDAL.Interfaces;
namespace TCMSBLL.Implementations;
///
/// Business logic for test runs and run results.
/// Includes Mochawesome JSON ingestion for Cypress CI webhook.
///
public class TCMSTestRunBLL : ITCMSTestRunBLL
{
private readonly ITCMSTestRunDAL _Dal;
private readonly ITCMSTestCaseDAL _TestCaseDal;
private readonly ITCMSCoverageDAL _CoverageDal;
private readonly ILogger _Logger;
public TCMSTestRunBLL(
ITCMSTestRunDAL dal,
ITCMSTestCaseDAL testCaseDal,
ITCMSCoverageDAL coverageDal,
ILogger logger)
{
_Dal = dal;
_TestCaseDal = testCaseDal;
_CoverageDal = coverageDal;
_Logger = logger;
}
public async Task GetTestRunAsync(
int testRunId, LoginDTO login, CancellationToken ct)
=> await _Dal.GetTestRunAsync(testRunId, login, ct).ConfigureAwait(false);
public async Task> GetTestRunListAsync(
int? projectId, int? moduleId, string? runType, string? status,
LoginDTO login, CancellationToken ct)
=> await _Dal.GetTestRunListAsync(projectId, moduleId, runType, status, login, ct).ConfigureAwait(false);
public async Task> GetRunResultsAsync(
int testRunId, LoginDTO login, CancellationToken ct)
=> await _Dal.GetRunResultsAsync(testRunId, login, ct).ConfigureAwait(false);
public async Task> GetDashboardAsync(
int? projectId, LoginDTO login, CancellationToken ct)
=> await _Dal.GetDashboardAsync(projectId, login, ct).ConfigureAwait(false);
public async Task SaveTestRunAsync(
TCMSTestRunDTO dto, LoginDTO login, CancellationToken ct)
{
try
{
if (string.IsNullOrWhiteSpace(dto.RunName))
return "Run Name is required.";
if (dto.ProjectId == 0)
return "Project is required.";
bool isNew = dto.TestRunId == 0;
if (isNew)
{
int seq = await _Dal.GetNextRunSequenceAsync(login, ct).ConfigureAwait(false);
dto.RunCode = $"RUN-{DateTime.UtcNow.Year}-{seq:D4}";
int newId = await _Dal.SaveTestRunAsync(dto, login, ct).ConfigureAwait(false);
return $"{SuccessResponse.SaveSuccessMessage} {newId}";
}
else
{
await _Dal.UpdateTestRunStatusAsync(dto.TestRunId, dto.Status, login.UserId, login, ct).ConfigureAwait(false);
return SuccessResponse.UpdateSuccess;
}
}
catch (Exception ex)
{
_Logger.LogError(ex, "SaveTestRun failed for project {ProjectId}", dto.ProjectId);
throw;
}
}
public async Task UpdateTestRunStatusAsync(
int testRunId, string status, LoginDTO login, CancellationToken ct)
{
await _Dal.UpdateTestRunStatusAsync(testRunId, status, login.UserId, login, ct).ConfigureAwait(false);
return SuccessResponse.UpdateSuccess;
}
public async Task UpdateRunResultAsync(
TCMSRunResultDTO dto, LoginDTO login, CancellationToken ct)
{
try
{
await _Dal.UpsertRunResultAsync(dto, login, ct).ConfigureAwait(false);
// Denormalize last run status on test case
await _TestCaseDal.UpdateLastRunStatusAsync(dto.TestCaseId, dto.Result, login, ct).ConfigureAwait(false);
return SuccessResponse.UpdateSuccess;
}
catch (Exception ex)
{
_Logger.LogError(ex, "UpdateRunResult failed for TC {TestCaseId}", dto.TestCaseId);
throw;
}
}
///
/// Ingests a Cypress Mochawesome JSON report.
///
/// Mochawesome structure (simplified):
/// { stats: {...}, results: [{ suites: [{ tests: [{ title: "...", pass: bool, err: {...} }] }] }] }
///
/// TC-ID extraction: regex /@TC-[A-Z]+-\d+/ in test title.
/// Maps each test result → TTCMSRUNRESULT row.
/// Creates a coverage snapshot for each affected module after ingestion.
///
public async Task IngestCypressReportAsync(
int projectId, int moduleId, string mochawesomeJson,
LoginDTO login, CancellationToken ct)
{
try
{
using var doc = JsonDocument.Parse(mochawesomeJson);
var root = doc.RootElement;
// Create the test run
int seq = await _Dal.GetNextRunSequenceAsync(login, ct).ConfigureAwait(false);
var run = new TCMSTestRunDTO
{
RunCode = $"RUN-{DateTime.UtcNow.Year}-{seq:D4}",
RunName = $"Cypress CI — {DateTime.UtcNow:yyyy-MM-dd HH:mm}",
ProjectId = projectId,
ModuleId = moduleId,
RunType = "Cypress",
SuiteType = "Regression",
Status = "Completed",
ActualStart = DateTime.UtcNow
};
int runId = await _Dal.SaveTestRunAsync(run, login, ct).ConfigureAwait(false);
// Parse results and look for @TC-XXX-NNN tags
var tcIdRegex = new System.Text.RegularExpressions.Regex(@"@TC-[A-Z]+-\d+");
int passed = 0, failed = 0, skipped = 0;
if (root.TryGetProperty("results", out var results))
{
foreach (var result in results.EnumerateArray())
{
if (!result.TryGetProperty("suites", out var suites)) continue;
foreach (var suite in suites.EnumerateArray())
{
if (!suite.TryGetProperty("tests", out var tests)) continue;
foreach (var test in tests.EnumerateArray())
{
string title = test.TryGetProperty("title", out var t) ? t.GetString() ?? "" : "";
bool isPending = test.TryGetProperty("pending", out var pend) && pend.GetBoolean();
bool isPass = test.TryGetProperty("pass", out var p) && p.GetBoolean();
string resultStr = isPending ? "Skip" : isPass ? "Pass" : "Fail";
string? failReason = null;
if (!isPass && !isPending && test.TryGetProperty("err", out var err))
{
err.TryGetProperty("message", out var msg);
failReason = msg.GetString();
}
int? durationMs = test.TryGetProperty("duration", out var dur) ? dur.GetInt32() : null;
// Extract TC-ID
var match = tcIdRegex.Match(title);
if (match.Success)
{
string tcIdTag = match.Value.TrimStart('@');
// Look up test case ID by TcId
var tcList = await _TestCaseDal.GetTestCaseListAsync(
moduleId, null, null, null, null, login, ct).ConfigureAwait(false);
var tc = tcList.FirstOrDefault(x => x.TcId == tcIdTag);
if (tc is not null)
{
await _Dal.UpsertRunResultAsync(new TCMSRunResultDTO
{
TestRunId = runId,
TestCaseId = tc.TestCaseId,
Result = resultStr,
FailureReason = failReason,
ExecutionMs = durationMs,
ExecutedBy = "Cypress CI",
TenantId = login.ClientId
}, login, ct).ConfigureAwait(false);
await _TestCaseDal.UpdateLastRunStatusAsync(tc.TestCaseId, resultStr, login, ct).ConfigureAwait(false);
if (resultStr == "Pass") passed++;
else if (resultStr == "Fail") failed++;
else skipped++;
}
}
}
}
}
}
// Mark run as completed
await _Dal.UpdateTestRunStatusAsync(runId, "Completed", login.UserId, login, ct).ConfigureAwait(false);
// Write coverage snapshot
int total = passed + failed + skipped;
if (total > 0)
{
await _CoverageDal.SaveCoverageSnapshotAsync(new TCMSCoverageSnapshotDTO
{
ModuleId = moduleId,
TestRunId = runId,
TotalCases = total,
PassedCases = passed,
CoveragePct = Math.Round((decimal)passed / total * 100, 2),
TenantId = login.ClientId
}, login, ct).ConfigureAwait(false);
}
return $"Cypress report ingested. Run: RUN-{DateTime.UtcNow.Year}-{seq:D4}. Pass: {passed} Fail: {failed} Skip: {skipped}";
}
catch (JsonException jex)
{
_Logger.LogError(jex, "IngestCypressReport: invalid JSON");
return "Invalid Mochawesome JSON format.";
}
catch (Exception ex)
{
_Logger.LogError(ex, "IngestCypressReport failed for module {ModuleId}", moduleId);
throw;
}
}
}