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