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 StrategicGeneralLedgerControllerTests { private StrategicGeneralLedgerController _controller; private Mock _mockOpportunityService; private Mock _mockDetailService; [SetUp] public void SetUp() { _mockOpportunityService = new Mock(); _mockDetailService = new Mock(); _controller = new StrategicGeneralLedgerController(_mockOpportunityService.Object, _mockDetailService.Object); } // --- GetOpportunityGL --- [Test] public async Task GetOpportunityGL_ReturnsOk_WhenFound() { const long opportunityId = 1; var data = new StrategicOpportunityGLEditData(); _mockOpportunityService .Setup(s => s.GetStrategicOpportunityGL(opportunityId, It.IsAny())) .ReturnsAsync(data); var result = await _controller.GetOpportunityGL(opportunityId, CancellationToken.None); result.Result.Should().BeOfType() .Which.Value.Should().Be(data); } [Test] public async Task GetOpportunityGL_ReturnsNotFound_WhenMissing() { _mockOpportunityService .Setup(s => s.GetStrategicOpportunityGL(It.IsAny(), It.IsAny())) .ReturnsAsync((StrategicOpportunityGLEditData)null); var result = await _controller.GetOpportunityGL(99, CancellationToken.None); result.Result.Should().BeOfType(); } // --- CreateOpportunityGeneralLedger --- [Test] public async Task CreateOpportunityGeneralLedger_ReturnsOk_WithNewId_WhenSuccessful() { const long newId = 42; var dto = new StrategicOpportunityGLDto(); _mockOpportunityService .Setup(s => s.CreateOpportunityGeneralLedgerAsync(dto, It.IsAny())) .ReturnsAsync(newId); var result = await _controller.CreateOpportunityGeneralLedger(dto, CancellationToken.None); result.Result.Should().BeOfType() .Which.Value.Should().Be(newId); } [Test] public async Task CreateOpportunityGeneralLedger_ReturnsBadRequest_WhenServiceReturnsZero() { _mockOpportunityService .Setup(s => s.CreateOpportunityGeneralLedgerAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(0L); var result = await _controller.CreateOpportunityGeneralLedger(new StrategicOpportunityGLDto(), CancellationToken.None); result.Result.Should().BeOfType(); } // --- UpdateOpportunityGeneralLedger --- [Test] public async Task UpdateOpportunityGeneralLedger_ReturnsOk_WhenSuccessful() { const long opportunityId = 5; _mockOpportunityService .Setup(s => s.UpdateOpportunityGeneralLedger(opportunityId, It.IsAny(), It.IsAny())) .ReturnsAsync(opportunityId); var result = await _controller.UpdateOpportunityGeneralLedger(opportunityId, new StrategicOpportunityGLUpdateDto(), CancellationToken.None); result.Result.Should().BeOfType(); } [Test] public async Task UpdateOpportunityGeneralLedger_ReturnsNotFound_WhenServiceReturnsZero() { _mockOpportunityService .Setup(s => s.UpdateOpportunityGeneralLedger(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(0L); var result = await _controller.UpdateOpportunityGeneralLedger(99, new StrategicOpportunityGLUpdateDto(), CancellationToken.None); result.Result.Should().BeOfType(); } // --- DeleteOpportunityGeneralLedger --- [Test] public async Task DeleteOpportunityGeneralLedger_ReturnsNotFound_WhenOpportunityDoesNotExist() { _mockOpportunityService .Setup(s => s.ContainsGLAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(false); var result = await _controller.DeleteOpportunityGeneralLedger(99, CancellationToken.None); result.Should().BeOfType(); _mockOpportunityService.Verify(s => s.DeleteGeneralLedgerOpportunityAsync(It.IsAny(), It.IsAny()), Times.Never); } [Test] public async Task DeleteOpportunityGeneralLedger_ReturnsNoContent_WhenDeleted() { const long opportunityId = 7; _mockOpportunityService.Setup(s => s.ContainsGLAsync(opportunityId, It.IsAny())).ReturnsAsync(true); _mockOpportunityService.Setup(s => s.DeleteGeneralLedgerOpportunityAsync(opportunityId, It.IsAny())).ReturnsAsync(true); var result = await _controller.DeleteOpportunityGeneralLedger(opportunityId, CancellationToken.None); result.Should().BeOfType(); } [Test] public async Task DeleteOpportunityGeneralLedger_ReturnsBadRequest_WhenDeleteFails() { const long opportunityId = 7; _mockOpportunityService.Setup(s => s.ContainsGLAsync(opportunityId, It.IsAny())).ReturnsAsync(true); _mockOpportunityService.Setup(s => s.DeleteGeneralLedgerOpportunityAsync(opportunityId, It.IsAny())).ReturnsAsync(false); var result = await _controller.DeleteOpportunityGeneralLedger(opportunityId, CancellationToken.None); result.Should().BeOfType(); } // --- GetGLDetail --- [Test] public async Task GetGLDetail_ReturnsOk_WhenFound() { const long opportunityId = 3; var dimensions = new[] { "system", "entity" }; var data = new StrategicOpportunityGLDetailData(); _mockDetailService .Setup(s => s.GetStrategicOpportunityGLDetailData(opportunityId, false, dimensions, It.IsAny())) .ReturnsAsync(data); var result = await _controller.GetGLDetail(opportunityId, dimensions, CancellationToken.None); result.Result.Should().BeOfType() .Which.Value.Should().Be(data); } [Test] public async Task GetGLDetail_ReturnsNotFound_WhenMissing() { _mockDetailService .Setup(s => s.GetStrategicOpportunityGLDetailData(It.IsAny(), false, It.IsAny(), It.IsAny())) .ReturnsAsync((StrategicOpportunityGLDetailData)null); var result = await _controller.GetGLDetail(99, Array.Empty(), CancellationToken.None); result.Result.Should().BeOfType(); } [Test] public async Task GetGLDetail_PassesIsTrackingFalse_ToService() { const long opportunityId = 3; var dimensions = new[] { "system" }; _mockDetailService .Setup(s => s.GetStrategicOpportunityGLDetailData(opportunityId, false, dimensions, It.IsAny())) .ReturnsAsync(new StrategicOpportunityGLDetailData()); await _controller.GetGLDetail(opportunityId, dimensions, CancellationToken.None); _mockDetailService.Verify(s => s.GetStrategicOpportunityGLDetailData(opportunityId, false, dimensions, CancellationToken.None), Times.Once); } // --- GetGLTrackingDetail --- [Test] public async Task GetGLTrackingDetail_ReturnsOk_WhenFound() { const long opportunityId = 4; var dimensions = new[] { "system", "glAccount" }; var data = new StrategicOpportunityGLDetailData(); _mockDetailService .Setup(s => s.GetStrategicOpportunityGLDetailData(opportunityId, true, dimensions, It.IsAny())) .ReturnsAsync(data); var result = await _controller.GetGLTrackingDetail(opportunityId, dimensions, CancellationToken.None); result.Result.Should().BeOfType() .Which.Value.Should().Be(data); } [Test] public async Task GetGLTrackingDetail_ReturnsNotFound_WhenMissing() { _mockDetailService .Setup(s => s.GetStrategicOpportunityGLDetailData(It.IsAny(), true, It.IsAny(), It.IsAny())) .ReturnsAsync((StrategicOpportunityGLDetailData)null); var result = await _controller.GetGLTrackingDetail(99, Array.Empty(), CancellationToken.None); result.Result.Should().BeOfType(); } [Test] public async Task GetGLTrackingDetail_PassesIsTrackingTrue_ToService() { const long opportunityId = 4; var dimensions = new[] { "system" }; _mockDetailService .Setup(s => s.GetStrategicOpportunityGLDetailData(opportunityId, true, dimensions, It.IsAny())) .ReturnsAsync(new StrategicOpportunityGLDetailData()); await _controller.GetGLTrackingDetail(opportunityId, dimensions, CancellationToken.None); _mockDetailService.Verify(s => s.GetStrategicOpportunityGLDetailData(opportunityId, true, dimensions, CancellationToken.None), Times.Once); } // --- UpdateGLDetails --- [Test] public async Task UpdateGLDetails_ReturnsBadRequest_WhenDtoIsNull() { var result = await _controller.UpdateGLDetails(1, null, CancellationToken.None); result.Should().BeOfType(); _mockDetailService.Verify(s => s.UpdateStrategicOpportunityGLDetailsData( It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Never); } [Test] public async Task UpdateGLDetails_ReturnsBadRequest_WhenAdjustmentsIsEmpty() { var dto = new UpdateGLDetailsDto { Adjustments = new List(), Rows = new List { "system", "entity" } }; var result = await _controller.UpdateGLDetails(1, dto, CancellationToken.None); result.Should().BeOfType(); _mockDetailService.Verify(s => s.UpdateStrategicOpportunityGLDetailsData( It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Never); } [Test] public async Task UpdateGLDetails_ReturnsBadRequest_WhenRowsIsEmpty() { var dto = new UpdateGLDetailsDto { Adjustments = new List { new() { NodeKey = "101", GoalAmountValue = 5000m } }, Rows = new List() }; var result = await _controller.UpdateGLDetails(1, dto, CancellationToken.None); result.Should().BeOfType(); _mockDetailService.Verify(s => s.UpdateStrategicOpportunityGLDetailsData( It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny()), Times.Never); } [Test] public async Task UpdateGLDetails_ReturnsOk_AndCallsService_WhenDtoIsValid() { const long opportunityId = 42; var dto = new UpdateGLDetailsDto { Adjustments = new List { new() { NodeKey = "101", GoalAmountValue = 5000m } }, Rows = new List { "system", "entity", "glAccount" } }; var result = await _controller.UpdateGLDetails(opportunityId, dto, CancellationToken.None); result.Should().BeOfType(); _mockDetailService.Verify(s => s.UpdateStrategicOpportunityGLDetailsData( opportunityId, dto.Adjustments, dto.Rows, CancellationToken.None), Times.Once); } // --- Route attribute assertions --- [Test] public void Controller_HasExpectedRouteTemplate() { var routeAttr = typeof(StrategicGeneralLedgerController).GetCustomAttribute(); routeAttr.Should().NotBeNull(); routeAttr.Template.Should().Be("api/v{api-version:apiVersion}/[controller]"); } [TestCase(nameof(StrategicGeneralLedgerController.GetOpportunityGL), typeof(HttpGetAttribute), "{opportunityId}/[action]")] [TestCase(nameof(StrategicGeneralLedgerController.CreateOpportunityGeneralLedger), typeof(HttpPostAttribute), null)] [TestCase(nameof(StrategicGeneralLedgerController.UpdateOpportunityGeneralLedger), typeof(HttpPutAttribute), "{opportunityId}")] [TestCase(nameof(StrategicGeneralLedgerController.DeleteOpportunityGeneralLedger), typeof(HttpDeleteAttribute), "{opportunityId}")] [TestCase(nameof(StrategicGeneralLedgerController.GetGLDetail), typeof(HttpPostAttribute), "{opportunityId}/detail")] [TestCase(nameof(StrategicGeneralLedgerController.GetGLTrackingDetail), typeof(HttpPostAttribute), "{opportunityId}/tracking-detail")] [TestCase(nameof(StrategicGeneralLedgerController.UpdateGLDetails), typeof(HttpPutAttribute), "{opportunityId}/details")] public void ActionRoute_HasExpectedHttpMethodAndTemplate(string methodName, Type httpAttrType, string expectedTemplate) { var method = typeof(StrategicGeneralLedgerController).GetMethod(methodName); method.Should().NotBeNull($"method '{methodName}' should exist on {nameof(StrategicGeneralLedgerController)}"); 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); } } }