using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using GB5Shared.DTO.Framework.Login; using GB5Shared.DTO.Promotion; using Moq; using Newtonsoft.Json; using WiBLL.Promotion; using WiBLL.WiMaster; using WiDAL.CustomCode.Promotion; using WiDAL.DTOs; using Xunit; namespace WiTests; /// /// WiHeaderPromotionAdapter — the load-bearing behaviour under test is that an update /// (existingDestinationId resolves to a real row) never wipes the destination's own /// Sections/Steps/SkillRequirements/CompetencyRequirements, because WiMasterDAL.SaveWiFull's own /// update path unconditionally soft-deletes every child row for that WiId before inserting whatever /// the current request's child collections contain. See the adapter's own class header comment. /// public class WiHeaderPromotionAdapterTests { private static LoginDTO Login() => new() { ClientId = -1, UserId = 7 }; private static WiHeaderDTO CleanDto(int wiId = 42) => new() { WiId = wiId, WiProcessId = 10, WiSubProcessId = 20, WiCode = "WI-ASSY-001", WiTitle = "Assemble Widget", ActivityType = ActivityType.Cycle, WiVersion = "1.0", WiStatus = WiStatus.Published, DisplayContext = DisplayContext.All, StepAdvanceMode = StepAdvanceMode.Manual, StdCycleTimeMins = 5, IsActive = 1, Status = 1, SortOrder = 1 }; private static (Mock bll, Mock processLookup, Mock subProcessLookup, Mock headerLookup) Mocks() => (new Mock(), new Mock(), new Mock(), new Mock()); private static WiHeaderPromotionAdapter NewAdapter( (Mock bll, Mock processLookup, Mock subProcessLookup, Mock headerLookup) m) => new(m.bll.Object, m.processLookup.Object, m.subProcessLookup.Object, m.headerLookup.Object); private static async Task BuildBundle(WiHeaderDTO dto) { var m = Mocks(); m.bll.Setup(b => b.GetWiById(dto.WiId, It.IsAny(), It.IsAny())) .ReturnsAsync(JsonConvert.SerializeObject(dto)); m.processLookup.Setup(l => l.GetWiProcessCodeById(dto.WiProcessId, It.IsAny(), It.IsAny())) .ReturnsAsync("ASSEMBLY"); m.subProcessLookup.Setup(l => l.GetWiSubProcessCodeById(dto.WiSubProcessId, It.IsAny(), It.IsAny())) .ReturnsAsync("SUBASSY1"); var adapter = NewAdapter(m); return await adapter.ExportForPromotionAsync(dto.WiId, Login(), CancellationToken.None); } [Fact] public async Task Validate_CleanWorkInstruction_IsOk() { var bundle = await BuildBundle(CleanDto()); var adapter = NewAdapter(Mocks()); var result = await adapter.ValidateForPromotionAsync(bundle, CancellationToken.None); Assert.True(result.IsPromotable); } [Fact] public async Task Validate_ApprovedByIdSet_Blocks() { var dto = CleanDto(); dto.ApprovedById = 999; var bundle = await BuildBundle(dto); var adapter = NewAdapter(Mocks()); var result = await adapter.ValidateForPromotionAsync(bundle, CancellationToken.None); Assert.False(result.IsPromotable); Assert.Contains(result.BlockingReasons, r => r.Contains("ApprovedById")); } [Fact] public async Task Import_FreshInsert_ResolvesProcessAndSubProcess() { var bundle = await BuildBundle(CleanDto()); var m = Mocks(); m.headerLookup.Setup(l => l.GetWiIdByCode("WI-ASSY-001", It.IsAny(), It.IsAny())) .ReturnsAsync((int?)null); m.processLookup.Setup(l => l.GetWiProcessIdByCode("ASSEMBLY", It.IsAny(), It.IsAny())) .ReturnsAsync(101); m.subProcessLookup.Setup(l => l.GetWiSubProcessIdByCode("SUBASSY1", It.IsAny(), It.IsAny())) .ReturnsAsync(201); WiSaveRequestDTO? captured = null; m.bll.Setup(b => b.SaveWiFull(It.IsAny(), It.IsAny(), It.IsAny())) .Callback((r, _, _) => { captured = r; r.Header.WiId = 904; }) .ReturnsAsync("Saved"); var adapter = NewAdapter(m); var newId = await adapter.ImportPromotedAsync(bundle, destinationSourceType: 3, existingDestinationId: null, Login(), CancellationToken.None); Assert.Equal(904, newId); Assert.NotNull(captured); Assert.Equal(101, captured!.Header.WiProcessId); Assert.Equal(201, captured.Header.WiSubProcessId); Assert.Equal(0, captured.Header.ApprovedById); Assert.Empty(captured.Sections); m.bll.Verify(b => b.GetWiFullDetail(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never, "a fresh insert has nothing existing to read back before saving"); } [Fact] public async Task Import_ExistingDestination_PreservesDestinationsOwnChildCollections() { var bundle = await BuildBundle(CleanDto()); // The destination's OWN current row already has real children — these must survive // untouched through Import, since SaveWiFull's own DAL soft-deletes every child row before // rebuilding from whatever the passed-in request carries. var existingDestinationDto = CleanDto(wiId: 700); existingDestinationDto.WiTitle = "Old Title At Destination"; // will be overwritten by promoted value var existingFullDetail = new WiSaveRequestDTO { Header = existingDestinationDto, Sections = new List { new() { WiSectionId = 1, WiId = 700, SectionSlNo = 1, SectionTitle = "Real Section" } }, SkillRequirements = new List { new() { WiSkillRequirementId = 1, WiId = 700, SkillId = 5, SkillName = "Soldering" } }, CompetencyRequirements = new List { new() { WiCompetencyRequirementId = 1, WiId = 700, CompetencyId = 9, CompetencyName = "Quality Check" } } }; var m = Mocks(); m.bll.Setup(b => b.GetWiFullDetail(700, It.IsAny(), It.IsAny())) .ReturnsAsync(JsonConvert.SerializeObject(existingFullDetail)); m.processLookup.Setup(l => l.GetWiProcessIdByCode("ASSEMBLY", It.IsAny(), It.IsAny())) .ReturnsAsync(101); m.subProcessLookup.Setup(l => l.GetWiSubProcessIdByCode("SUBASSY1", It.IsAny(), It.IsAny())) .ReturnsAsync(201); WiSaveRequestDTO? captured = null; m.bll.Setup(b => b.SaveWiFull(It.IsAny(), It.IsAny(), It.IsAny())) .Callback((r, _, _) => captured = r) .ReturnsAsync("Updated"); var adapter = NewAdapter(m); var resultId = await adapter.ImportPromotedAsync(bundle, destinationSourceType: 2, existingDestinationId: 700, Login(), CancellationToken.None); Assert.Equal(700, resultId); Assert.NotNull(captured); // Parent field overwritten with the promoted value... Assert.Equal("Assemble Widget", captured!.Header.WiTitle); // ...but every child collection still carries exactly what the destination already had. Assert.Single(captured.Sections); Assert.Equal("Real Section", captured.Sections[0].SectionTitle); Assert.Single(captured.SkillRequirements); Assert.Equal("Soldering", captured.SkillRequirements[0].SkillName); Assert.Single(captured.CompetencyRequirements); Assert.Equal("Quality Check", captured.CompetencyRequirements[0].CompetencyName); } [Fact] public async Task Import_CodeAlreadyExistsAtDestination_UpdatesRatherThanDuplicating() { var bundle = await BuildBundle(CleanDto()); var m = Mocks(); m.headerLookup.Setup(l => l.GetWiIdByCode("WI-ASSY-001", It.IsAny(), It.IsAny())) .ReturnsAsync(700); m.bll.Setup(b => b.GetWiFullDetail(700, It.IsAny(), It.IsAny())) .ReturnsAsync(JsonConvert.SerializeObject(new WiSaveRequestDTO { Header = CleanDto(wiId: 700) })); m.processLookup.Setup(l => l.GetWiProcessIdByCode("ASSEMBLY", It.IsAny(), It.IsAny())) .ReturnsAsync(101); m.subProcessLookup.Setup(l => l.GetWiSubProcessIdByCode("SUBASSY1", It.IsAny(), It.IsAny())) .ReturnsAsync(201); int? capturedId = null; m.bll.Setup(b => b.SaveWiFull(It.IsAny(), It.IsAny(), It.IsAny())) .Callback((r, _, _) => capturedId = r.Header.WiId) .ReturnsAsync("Updated"); var adapter = NewAdapter(m); var resultId = await adapter.ImportPromotedAsync(bundle, 3, existingDestinationId: null, Login(), CancellationToken.None); Assert.Equal(700, resultId); Assert.Equal(700, capturedId); } }