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; } } }