using FluentAssertions; using Microsoft.AspNetCore.Mvc; using Moq; using NUnit.Framework; using Strata.ContinuousImprovement.Api.Controllers; using Strata.ContinuousImprovement.Biz.StrategicOpportunities; using Strata.ContinuousImprovement.Biz.StrategicOpportunities.Dtos; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Threading; using System.Threading.Tasks; namespace Strata.ContinuousImprovement.Api.Test.Unit.Controllers { [ExcludeFromCodeCoverage] [TestFixture, Category("Unit"), Category("Controller")] public class StrategicChargeCodeControllerTests { private StrategicChargeCodeController _controller; private Mock _mockOpportunityService; private Mock _mockDetailService; [SetUp] public void SetUp() { _mockOpportunityService = new Mock(); _mockDetailService = new Mock(); _controller = new StrategicChargeCodeController(_mockOpportunityService.Object, _mockDetailService.Object); } // --- GetOpportunityChargeCode --- [Test] public async Task GetOpportunityChargeCode_ReturnsOk_WhenFound() { const long opportunityId = 1; var data = new StrategicOpportunityChargeCodeEditData(); _mockOpportunityService .Setup(s => s.GetStrategicOpportunityChargeCode(opportunityId, It.IsAny())) .ReturnsAsync(data); var result = await _controller.GetOpportunityChargeCode(opportunityId, CancellationToken.None); result.Result.Should().BeOfType() .Which.Value.Should().Be(data); } [Test] public async Task GetOpportunityChargeCode_ReturnsNotFound_WhenMissing() { _mockOpportunityService .Setup(s => s.GetStrategicOpportunityChargeCode(It.IsAny(), It.IsAny())) .ReturnsAsync((StrategicOpportunityChargeCodeEditData)null); var result = await _controller.GetOpportunityChargeCode(99, CancellationToken.None); result.Result.Should().BeOfType(); } // --- CreateOpportunityChargeCode --- [Test] public async Task CreateOpportunityChargeCode_ReturnsOk_WithNewId_WhenSuccessful() { const long newId = 42; var dto = new StrategicOpportunityChargeCodeDto(); _mockOpportunityService .Setup(s => s.CreateOpportunityChargeCodeAsync(dto, It.IsAny())) .ReturnsAsync(newId); var result = await _controller.CreateOpportunityChargeCode(dto, CancellationToken.None); result.Result.Should().BeOfType() .Which.Value.Should().Be(newId); } [Test] public async Task CreateOpportunityChargeCode_ReturnsBadRequest_WhenServiceReturnsZero() { _mockOpportunityService .Setup(s => s.CreateOpportunityChargeCodeAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(0L); var result = await _controller.CreateOpportunityChargeCode(new StrategicOpportunityChargeCodeDto(), CancellationToken.None); result.Result.Should().BeOfType(); } // --- UpdateOpportunityChargeCode --- [Test] public async Task UpdateOpportunityChargeCode_ReturnsOk_WhenSuccessful() { const long opportunityId = 5; _mockOpportunityService .Setup(s => s.UpdateOpportunityChargeCode(opportunityId, It.IsAny(), It.IsAny())) .ReturnsAsync(opportunityId); var result = await _controller.UpdateOpportunityChargeCode(opportunityId, new StrategicOpportunityChargeCodeUpdateDto(), CancellationToken.None); result.Result.Should().BeOfType(); } [Test] public async Task UpdateOpportunityChargeCode_ReturnsNotFound_WhenServiceReturnsZero() { _mockOpportunityService .Setup(s => s.UpdateOpportunityChargeCode(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(0L); var result = await _controller.UpdateOpportunityChargeCode(99, new StrategicOpportunityChargeCodeUpdateDto(), CancellationToken.None); result.Result.Should().BeOfType(); } // --- DeleteOpportunityChargeCode --- [Test] public async Task DeleteOpportunityChargeCode_ReturnsNotFound_WhenOpportunityDoesNotExist() { _mockOpportunityService .Setup(s => s.ContainsAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(false); var result = await _controller.DeleteOpportunityChargeCode(99, CancellationToken.None); result.Should().BeOfType(); _mockOpportunityService.Verify(s => s.DeleteChargeCodeOpportunityAsync(It.IsAny(), It.IsAny()), Times.Never); } [Test] public async Task DeleteOpportunityChargeCode_ReturnsNoContent_WhenDeleted() { const long opportunityId = 7; _mockOpportunityService.Setup(s => s.ContainsAsync(opportunityId, It.IsAny())).ReturnsAsync(true); _mockOpportunityService.Setup(s => s.DeleteChargeCodeOpportunityAsync(opportunityId, It.IsAny())).ReturnsAsync(true); var result = await _controller.DeleteOpportunityChargeCode(opportunityId, CancellationToken.None); result.Should().BeOfType(); } [Test] public async Task DeleteOpportunityChargeCode_ReturnsBadRequest_WhenDeleteFails() { const long opportunityId = 7; _mockOpportunityService.Setup(s => s.ContainsAsync(opportunityId, It.IsAny())).ReturnsAsync(true); _mockOpportunityService.Setup(s => s.DeleteChargeCodeOpportunityAsync(opportunityId, It.IsAny())).ReturnsAsync(false); var result = await _controller.DeleteOpportunityChargeCode(opportunityId, CancellationToken.None); result.Should().BeOfType(); } // --- GetGoalDetail --- [Test] public async Task GetGoalDetail_ReturnsOk_WhenFound() { const long opportunityId = 3; var getData = new StrategicOpportunityGetDetailData(); var data = new StrategicOpportunityDetailData(); _mockDetailService .Setup(s => s.GetStrategicOpportunityDetailData(opportunityId, false, getData, It.IsAny())) .ReturnsAsync(data); var result = await _controller.GetGoalDetail(opportunityId, getData, CancellationToken.None); result.Result.Should().BeOfType() .Which.Value.Should().Be(data); } [Test] public async Task GetGoalDetail_ReturnsNotFound_WhenMissing() { _mockDetailService .Setup(s => s.GetStrategicOpportunityDetailData(It.IsAny(), false, It.IsAny(), It.IsAny())) .ReturnsAsync((StrategicOpportunityDetailData)null); var result = await _controller.GetGoalDetail(99, new StrategicOpportunityGetDetailData(), CancellationToken.None); result.Result.Should().BeOfType(); } [Test] public async Task GetGoalDetail_PassesIsTrackingFalse_ToService() { const long opportunityId = 3; var getData = new StrategicOpportunityGetDetailData(); _mockDetailService .Setup(s => s.GetStrategicOpportunityDetailData(opportunityId, false, getData, It.IsAny())) .ReturnsAsync(new StrategicOpportunityDetailData()); await _controller.GetGoalDetail(opportunityId, getData, CancellationToken.None); _mockDetailService.Verify(s => s.GetStrategicOpportunityDetailData(opportunityId, false, getData, CancellationToken.None), Times.Once); } // --- GetTrackingDetail --- [Test] public async Task GetTrackingDetail_ReturnsOk_WhenFound() { const long opportunityId = 4; var getData = new StrategicOpportunityGetDetailData(); var data = new StrategicOpportunityDetailData(); _mockDetailService .Setup(s => s.GetStrategicOpportunityDetailData(opportunityId, true, getData, It.IsAny())) .ReturnsAsync(data); var result = await _controller.GetTrackingDetail(opportunityId, getData, CancellationToken.None); result.Result.Should().BeOfType() .Which.Value.Should().Be(data); } [Test] public async Task GetTrackingDetail_ReturnsNotFound_WhenMissing() { _mockDetailService .Setup(s => s.GetStrategicOpportunityDetailData(It.IsAny(), true, It.IsAny(), It.IsAny())) .ReturnsAsync((StrategicOpportunityDetailData)null); var result = await _controller.GetTrackingDetail(99, new StrategicOpportunityGetDetailData(), CancellationToken.None); result.Result.Should().BeOfType(); } [Test] public async Task GetTrackingDetail_PassesIsTrackingTrue_ToService() { const long opportunityId = 4; var getData = new StrategicOpportunityGetDetailData(); _mockDetailService .Setup(s => s.GetStrategicOpportunityDetailData(opportunityId, true, getData, It.IsAny())) .ReturnsAsync(new StrategicOpportunityDetailData()); await _controller.GetTrackingDetail(opportunityId, getData, CancellationToken.None); _mockDetailService.Verify(s => s.GetStrategicOpportunityDetailData(opportunityId, true, getData, CancellationToken.None), Times.Once); } // --- UpdateDetails --- [Test] public async Task UpdateDetails_ReturnsBadRequest_WhenDtoIsNull() { var result = await _controller.UpdateDetails(1, null, CancellationToken.None); result.Should().BeOfType(); _mockDetailService.Verify(s => s.UpdateStrategicOpportunityDetailsData( It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Never); } [Test] public async Task UpdateDetails_ReturnsBadRequest_WhenAdjustmentsIsEmpty() { var dto = new StrategicChargeCodeController.UpdateDetailsDto { Adjustments = new List(), Rows = new List { "system", "entity" } }; var result = await _controller.UpdateDetails(1, dto, CancellationToken.None); result.Should().BeOfType(); _mockDetailService.Verify(s => s.UpdateStrategicOpportunityDetailsData( It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Never); } [Test] public async Task UpdateDetails_ReturnsBadRequest_WhenRowsIsEmpty() { var dto = new StrategicChargeCodeController.UpdateDetailsDto { Adjustments = new List { new() { NodeKey = "101", CostsValue = 5000m } }, Rows = new List() }; var result = await _controller.UpdateDetails(1, dto, CancellationToken.None); result.Should().BeOfType(); _mockDetailService.Verify(s => s.UpdateStrategicOpportunityDetailsData( It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Never); } [Test] public async Task UpdateDetails_ReturnsOk_AndCallsService_WhenDtoIsValid() { const long opportunityId = 42; var dto = new StrategicChargeCodeController.UpdateDetailsDto { Adjustments = new List { new() { NodeKey = "101", CostsValue = 5000m } }, Rows = new List { "system", "entity", "chargeCode" } }; var result = await _controller.UpdateDetails(opportunityId, dto, CancellationToken.None); result.Should().BeOfType(); _mockDetailService.Verify(s => s.UpdateStrategicOpportunityDetailsData( opportunityId, dto.Adjustments, dto.Rows, CancellationToken.None), Times.Once); } // --- Route attribute assertions --- [Test] public void Controller_HasExpectedRouteTemplate() { var routeAttr = typeof(StrategicChargeCodeController).GetCustomAttribute(); routeAttr.Should().NotBeNull(); routeAttr.Template.Should().Be("api/v{api-version:apiVersion}/[controller]"); } [TestCase(nameof(StrategicChargeCodeController.GetOpportunityChargeCode), typeof(HttpGetAttribute), "{opportunityId}/[action]")] [TestCase(nameof(StrategicChargeCodeController.CreateOpportunityChargeCode), typeof(HttpPostAttribute), null)] [TestCase(nameof(StrategicChargeCodeController.UpdateOpportunityChargeCode), typeof(HttpPutAttribute), "{opportunityId}")] [TestCase(nameof(StrategicChargeCodeController.DeleteOpportunityChargeCode), typeof(HttpDeleteAttribute), "{opportunityId}")] [TestCase(nameof(StrategicChargeCodeController.GetGoalDetail), typeof(HttpPostAttribute), "{opportunityId}/goal-detail")] [TestCase(nameof(StrategicChargeCodeController.ExportInitiative), typeof(HttpPostAttribute), "{opportunityId}/export")] [TestCase(nameof(StrategicChargeCodeController.ExportOpportunity), typeof(HttpPostAttribute), "{opportunityId}/export-opportunity")] [TestCase(nameof(StrategicChargeCodeController.GetTrackingDetail), typeof(HttpPostAttribute), "{opportunityId}/tracking-detail")] [TestCase(nameof(StrategicChargeCodeController.UpdateDetails), typeof(HttpPutAttribute), "{opportunityId}/details")] public void ActionRoute_HasExpectedHttpMethodAndTemplate(string methodName, Type httpAttrType, string expectedTemplate) { var method = typeof(StrategicChargeCodeController).GetMethod(methodName); method.Should().NotBeNull($"method '{methodName}' should exist on {nameof(StrategicChargeCodeController)}"); var attr = method.GetCustomAttribute(httpAttrType); attr.Should().NotBeNull($"{methodName} should be decorated with [{httpAttrType.Name}]"); var template = (string)attr.GetType().GetProperty("Template").GetValue(attr); template.Should().Be(expectedTemplate); } } }