using System.Net.Http;
using System.Net.Http.Json;
using System.Text;
using ClosedXML.Excel;
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;
///
/// Handles PIE import, Gherkin import, Gherkin export, and UAT checklist export.
///
/// PIE import flow:
/// 1. Call GET /PIE/QC/GetPIETestTemplates?moduleId={pieModuleId} via internal HTTP
/// 2. For each scenario: check PIESCENID not already imported (idempotent)
/// 3. Create MTCMSTESTCASE with TC-[MOD]-[SEQ] + parse JSONB steps into MTCMSTESTSTEP rows
/// 4. Update MTCMSPIESYNC with sync date + count
///
public class TCMSImportBLL : ITCMSImportBLL
{
private readonly ITCMSTestCaseDAL _TestCaseDal;
private readonly ITCMSModuleDAL _ModuleDal;
private readonly ITCMSImportDAL _ImportDal;
private readonly IHttpClientFactory _HttpFactory;
private readonly ILogger _Logger;
public TCMSImportBLL(
ITCMSTestCaseDAL testCaseDal,
ITCMSModuleDAL moduleDal,
ITCMSImportDAL importDal,
IHttpClientFactory httpFactory,
ILogger logger)
{
_TestCaseDal = testCaseDal;
_ModuleDal = moduleDal;
_ImportDal = importDal;
_HttpFactory = httpFactory;
_Logger = logger;
}
// ── PIE Import ───────────────────────────────────────────────────────────
public async Task ImportFromPIEAsync(
TCMSImportFromPIERequestDTO request, LoginDTO login, CancellationToken ct)
{
var result = new TCMSImportResultDTO();
try
{
var module = await _ModuleDal.GetModuleAsync(request.ModuleId, login, ct).ConfigureAwait(false);
if (module is null)
{
result.ErrorMessages.Add("Module not found.");
result.Errors++;
return result;
}
// Fetch PIE templates via internal API
var http = _HttpFactory.CreateClient("PIE");
var pieTemplates = await http.GetFromJsonAsync(
$"/PIE/QC/GetPIETestTemplates?moduleId={request.PieModuleId}",
ct).ConfigureAwait(false);
if (pieTemplates?.Data is null)
{
result.ErrorMessages.Add("No PIE templates returned.");
return result;
}
foreach (var template in pieTemplates.Data)
{
try
{
// Idempotency check
int? existing = await _TestCaseDal.GetByPieScenIdAsync(template.ScenId, login, ct).ConfigureAwait(false);
if (existing.HasValue && !request.ForceRefresh)
{
result.Skipped++;
continue;
}
string prefix = $"TC-{module.ModuleCode}";
int seq = await _TestCaseDal.GetNextSequenceAsync(prefix, login, ct).ConfigureAwait(false);
var tc = new TCMSTestCaseDTO
{
TcId = $"{prefix}-{seq:D3}",
Title = template.Title,
Preconditions = template.Preconditions,
ModuleId = request.ModuleId,
Priority = MapComplexityToPriority(template.ComplexityLevel),
TestType = "Functional",
ExecutionType = "Manual",
ScenarioType = "HappyPath",
Status = "Active",
PieScenId = template.ScenId,
PieVariantId = template.LinkedVariantId,
IsAutoGenerated = true,
TenantId = login.ClientId
};
int newId;
if (existing.HasValue && request.ForceRefresh)
{
tc.TestCaseId = existing.Value;
await _TestCaseDal.UpdateTestCaseAsync(tc, login, ct).ConfigureAwait(false);
newId = existing.Value;
result.Updated++;
}
else
{
newId = await _TestCaseDal.SaveTestCaseAsync(tc, login, ct).ConfigureAwait(false);
result.Created++;
}
// Parse JSONB steps (stored as JSON array of step strings)
if (!string.IsNullOrWhiteSpace(template.Steps))
{
var steps = ParsePieSteps(template.Steps, template.ExpectedResult);
if (steps.Count > 0)
await _TestCaseDal.SaveStepsAsync(newId, steps, login, ct).ConfigureAwait(false);
}
}
catch (Exception ex)
{
result.Errors++;
result.ErrorMessages.Add($"Scenario '{template.Title}': {ex.Message}");
_Logger.LogWarning(ex, "ImportFromPIE: failed for scenario {ScenId}", template.ScenId);
}
}
// Update sync record
await _ImportDal.UpsertPieSyncAsync(
request.ModuleId, request.PieModuleId,
result.Created + result.Updated, login, ct).ConfigureAwait(false);
}
catch (Exception ex)
{
_Logger.LogError(ex, "ImportFromPIE failed for module {ModuleId}", request.ModuleId);
result.Errors++;
result.ErrorMessages.Add(ex.Message);
}
return result;
}
// ── Gherkin Import ───────────────────────────────────────────────────────
public async Task ImportFromGherkinAsync(
TCMSImportFromGherkinRequestDTO request, LoginDTO login, CancellationToken ct)
{
var result = new TCMSImportResultDTO();
var module = await _ModuleDal.GetModuleAsync(request.ModuleId, login, ct).ConfigureAwait(false);
if (module is null)
{
result.ErrorMessages.Add("Module not found.");
result.Errors++;
return result;
}
// Simple Gherkin parser: splits on "Scenario:" and "Scenario Outline:"
var lines = request.GherkinContent.Split('\n');
var currentSteps = new List();
string? currentTitle = null;
string? currentFeature = null;
foreach (var rawLine in lines)
{
var line = rawLine.Trim();
if (line.StartsWith("Feature:", StringComparison.OrdinalIgnoreCase))
{
currentFeature = line[8..].Trim();
}
else if (line.StartsWith("Scenario:", StringComparison.OrdinalIgnoreCase) ||
line.StartsWith("Scenario Outline:", StringComparison.OrdinalIgnoreCase))
{
// Save previous scenario
if (currentTitle is not null)
await SaveParsedScenarioAsync(module, currentTitle, currentSteps, result, login, ct).ConfigureAwait(false);
currentTitle = line.Contains(':') ? line[(line.IndexOf(':') + 1)..].Trim() : line;
currentSteps = new List();
}
else if (line.StartsWith("Given ", StringComparison.OrdinalIgnoreCase) ||
line.StartsWith("When ", StringComparison.OrdinalIgnoreCase) ||
line.StartsWith("Then ", StringComparison.OrdinalIgnoreCase) ||
line.StartsWith("And ", StringComparison.OrdinalIgnoreCase) ||
line.StartsWith("But ", StringComparison.OrdinalIgnoreCase))
{
var stepType = line.Split(' ')[0];
var stepText = line[(stepType.Length + 1)..].Trim();
currentSteps.Add(new TCMSTestStepDTO { StepType = stepType, StepText = stepText });
}
}
// Save last scenario
if (currentTitle is not null)
await SaveParsedScenarioAsync(module, currentTitle, currentSteps, result, login, ct).ConfigureAwait(false);
return result;
}
// ── Gherkin Export ───────────────────────────────────────────────────────
public async Task ExportGherkinAsync(int moduleId, LoginDTO login, CancellationToken ct)
{
var module = await _ModuleDal.GetModuleAsync(moduleId, login, ct).ConfigureAwait(false);
if (module is null) return string.Empty;
var testCases = await _TestCaseDal.GetTestCaseListAsync(moduleId, null, null, "Active", true, login, ct).ConfigureAwait(false);
var sb = new StringBuilder();
sb.AppendLine($"Feature: {module.ModuleName}");
sb.AppendLine();
foreach (var tc in testCases)
{
var full = await _TestCaseDal.GetTestCaseAsync(tc.TestCaseId, login, ct).ConfigureAwait(false);
if (full is null) continue;
sb.AppendLine($" @{tc.TcId}");
sb.AppendLine($" Scenario: {tc.Title}");
if (!string.IsNullOrWhiteSpace(full.Preconditions))
{
sb.AppendLine($" # Preconditions: {full.Preconditions}");
}
var steps = await _TestCaseDal.GetStepsAsync(tc.TestCaseId, login, ct).ConfigureAwait(false);
foreach (var step in steps.OrderBy(s => s.StepOrder))
{
sb.AppendLine($" {step.StepType} {step.StepText}");
}
sb.AppendLine();
}
return sb.ToString();
}
// ── UAT Checklist Export (ClosedXML) ─────────────────────────────────────
public async Task ExportUATChecklistAsync(
int moduleId, int? testRunId, LoginDTO login, CancellationToken ct)
{
var module = await _ModuleDal.GetModuleAsync(moduleId, login, ct).ConfigureAwait(false);
var rows = await _ImportDal.GetUATChecklistRowsAsync(moduleId, testRunId, login, ct).ConfigureAwait(false);
using var wb = new XLWorkbook();
var ws = wb.Worksheets.Add($"{module?.ModuleName ?? "TCMS"} UAT Checklist");
// Header row
ws.Cell(1, 1).Value = "TC_ID";
ws.Cell(1, 2).Value = "Feature";
ws.Cell(1, 3).Value = "Scenario";
ws.Cell(1, 4).Value = "Priority";
ws.Cell(1, 5).Value = "Type";
ws.Cell(1, 6).Value = "Given";
ws.Cell(1, 7).Value = "When";
ws.Cell(1, 8).Value = "Then";
ws.Cell(1, 9).Value = "Tester";
ws.Cell(1, 10).Value = "Status";
ws.Cell(1, 11).Value = "Notes";
ws.Cell(1, 12).Value = "Sign-off";
var headerRow = ws.Row(1);
headerRow.Style.Font.Bold = true;
headerRow.Style.Fill.BackgroundColor = XLColor.FromHtml("#1E3A5F");
headerRow.Style.Font.FontColor = XLColor.White;
int rowNum = 2;
foreach (var r in rows)
{
ws.Cell(rowNum, 1).Value = r.TcId;
ws.Cell(rowNum, 2).Value = r.Feature;
ws.Cell(rowNum, 3).Value = r.Scenario;
ws.Cell(rowNum, 4).Value = r.Priority;
ws.Cell(rowNum, 5).Value = r.TestType;
ws.Cell(rowNum, 6).Value = r.Given;
ws.Cell(rowNum, 7).Value = r.When;
ws.Cell(rowNum, 8).Value = r.Then;
ws.Cell(rowNum, 9).Value = r.Tester ?? "";
ws.Cell(rowNum, 10).Value = r.Status ?? "";
ws.Cell(rowNum, 11).Value = r.Notes ?? "";
ws.Cell(rowNum, 12).Value = ""; // sign-off column left blank
// Conditional colour by status
if (r.Status == "Pass")
ws.Row(rowNum).Style.Fill.BackgroundColor = XLColor.FromHtml("#D5F5E3");
else if (r.Status == "Fail")
ws.Row(rowNum).Style.Fill.BackgroundColor = XLColor.FromHtml("#FADBD8");
rowNum++;
}
ws.Columns().AdjustToContents();
ws.Column(3).Width = 40; // Scenario column — fixed width
ws.Column(6).Width = 35; // Given
ws.Column(7).Width = 35; // When
ws.Column(8).Width = 35; // Then
using var ms = new MemoryStream();
wb.SaveAs(ms);
return ms.ToArray();
}
// ── Helpers ──────────────────────────────────────────────────────────────
private async Task SaveParsedScenarioAsync(
TCMSModuleDTO module, string title, List steps,
TCMSImportResultDTO result, LoginDTO login, CancellationToken ct)
{
try
{
string prefix = $"TC-{module.ModuleCode}";
int seq = await _TestCaseDal.GetNextSequenceAsync(prefix, login, ct).ConfigureAwait(false);
var tc = new TCMSTestCaseDTO
{
TcId = $"{prefix}-{seq:D3}",
Title = title,
ModuleId = module.ModuleId,
Priority = "P2",
TestType = "Functional",
ExecutionType = "UI",
ScenarioType = "HappyPath",
Status = "Active",
IsAutoGenerated = false,
TenantId = login.ClientId
};
int newId = await _TestCaseDal.SaveTestCaseAsync(tc, login, ct).ConfigureAwait(false);
if (steps.Count > 0)
await _TestCaseDal.SaveStepsAsync(newId, steps, login, ct).ConfigureAwait(false);
result.Created++;
}
catch (Exception ex)
{
result.Errors++;
result.ErrorMessages.Add($"Scenario '{title}': {ex.Message}");
}
}
private static List ParsePieSteps(string stepsJson, string? expectedResult)
{
var steps = new List();
try
{
// PIE stores steps as a JSON array of strings: ["step 1", "step 2", ...]
var stepTexts = System.Text.Json.JsonSerializer.Deserialize>(stepsJson)
?? new List();
int order = 1;
bool firstStep = true;
foreach (var text in stepTexts)
{
steps.Add(new TCMSTestStepDTO
{
StepOrder = order++,
StepType = firstStep ? "Given" : "And",
StepText = text
});
firstStep = false;
}
if (!string.IsNullOrWhiteSpace(expectedResult))
{
steps.Add(new TCMSTestStepDTO
{
StepOrder = order,
StepType = "Then",
StepText = expectedResult
});
}
}
catch { /* malformed JSONB — skip steps */ }
return steps;
}
private static string MapComplexityToPriority(short complexity) => complexity switch
{
1 => "P1",
2 => "P2",
_ => "P3"
};
// Internal DTO to deserialize PIE API response
private sealed class PIETemplateResponse
{
public List? Data { get; set; }
}
private sealed class PIETemplateItem
{
public Guid ScenId { get; set; }
public string Title { get; set; } = string.Empty;
public string? Preconditions { get; set; }
public string? Steps { get; set; }
public string? ExpectedResult { get; set; }
public short ComplexityLevel { get; set; }
public Guid? LinkedVariantId { get; set; }
}
}