using FluentAssertions; using Microsoft.Extensions.Time.Testing; using NSubstitute; using SIGCM2.Application.Abstractions.Persistence; using SIGCM2.Application.Avisos; using SIGCM2.Application.Rubros.GetById; using SIGCM2.Application.Rubros.GetTree; using SIGCM2.Domain.Entities; using SIGCM2.Domain.Exceptions; namespace SIGCM2.Application.Tests.Rubros.GetTree; public class GetRubroTreeQueryHandlerTests { private static readonly FakeTimeProvider FakeTime = new(new DateTimeOffset(2026, 4, 18, 12, 0, 0, TimeSpan.Zero)); private readonly IRubroRepository _repo = Substitute.For(); private readonly IAvisoQueryRepository _avisoQuery = Substitute.For(); private static Rubro MakeRubro(int id, int? parentId = null, bool activo = true) => new(id, parentId, $"Rubro{id}", 0, activo, null, FakeTime.GetUtcNow().UtcDateTime, null); // ── GetRubroTreeQueryHandler ───────────────────────────────────────────── [Fact] public async Task GetTree_OnlyActivos_ByDefault_ReturnsActiveTree() { _repo.GetAllAsync(false, Arg.Any()) .Returns(new[] { MakeRubro(1), MakeRubro(2) }); _avisoQuery.CountAvisosBatchAsync(Arg.Any>(), Arg.Any()) .Returns(new Dictionary()); var handler = new GetRubroTreeQueryHandler(_repo, _avisoQuery); var result = await handler.Handle(new GetRubroTreeQuery(IncluirInactivos: false)); result.Should().HaveCount(2); } [Fact] public async Task GetTree_IncluirInactivos_CallsRepoWithTrue() { _repo.GetAllAsync(true, Arg.Any()) .Returns(new[] { MakeRubro(1), MakeRubro(2, activo: false) }); _avisoQuery.CountAvisosBatchAsync(Arg.Any>(), Arg.Any()) .Returns(new Dictionary()); var handler = new GetRubroTreeQueryHandler(_repo, _avisoQuery); var result = await handler.Handle(new GetRubroTreeQuery(IncluirInactivos: true)); await _repo.Received(1).GetAllAsync(true, Arg.Any()); result.Should().HaveCount(2); // both roots } [Fact] public async Task GetTree_Empty_ReturnsEmptyList() { _repo.GetAllAsync(Arg.Any(), Arg.Any()) .Returns(Array.Empty()); _avisoQuery.CountAvisosBatchAsync(Arg.Any>(), Arg.Any()) .Returns(new Dictionary()); var handler = new GetRubroTreeQueryHandler(_repo, _avisoQuery); var result = await handler.Handle(new GetRubroTreeQuery(IncluirInactivos: false)); result.Should().BeEmpty(); } // ── CAT-002: TieneAvisos populated via batch ───────────────────────────── [Fact] public async Task Handle_PopulatesTieneAvisos_True_WhenBatchResultContainsCount() { _repo.GetAllAsync(false, Arg.Any()) .Returns(new[] { MakeRubro(1), MakeRubro(2), MakeRubro(3) }); _avisoQuery.CountAvisosBatchAsync(Arg.Any>(), Arg.Any()) .Returns(new Dictionary { { 1, 2 } }); var handler = new GetRubroTreeQueryHandler(_repo, _avisoQuery); var result = await handler.Handle(new GetRubroTreeQuery(IncluirInactivos: false)); result.Single(n => n.Id == 1).TieneAvisos.Should().BeTrue(); result.Single(n => n.Id == 2).TieneAvisos.Should().BeFalse(); result.Single(n => n.Id == 3).TieneAvisos.Should().BeFalse(); } [Fact] public async Task Handle_CallsBatchExactlyOnce_WithAllRubroIds() { _repo.GetAllAsync(false, Arg.Any()) .Returns(new[] { MakeRubro(1), MakeRubro(2), MakeRubro(3) }); _avisoQuery.CountAvisosBatchAsync(Arg.Any>(), Arg.Any()) .Returns(new Dictionary()); var handler = new GetRubroTreeQueryHandler(_repo, _avisoQuery); await handler.Handle(new GetRubroTreeQuery(IncluirInactivos: false)); await _avisoQuery.Received(1).CountAvisosBatchAsync( Arg.Any>(), Arg.Any()); } [Fact] public async Task Handle_EmptyTree_CallsBatchWithEmptyList() { _repo.GetAllAsync(Arg.Any(), Arg.Any()) .Returns(Array.Empty()); _avisoQuery.CountAvisosBatchAsync(Arg.Any>(), Arg.Any()) .Returns(new Dictionary()); var handler = new GetRubroTreeQueryHandler(_repo, _avisoQuery); await handler.Handle(new GetRubroTreeQuery(IncluirInactivos: false)); await _avisoQuery.Received(1).CountAvisosBatchAsync( Arg.Is>(ids => ids.Count == 0), Arg.Any()); } [Fact] public async Task Handle_StubBehavior_AllNodesTieneAvisosFalse() { _repo.GetAllAsync(false, Arg.Any()) .Returns(new[] { MakeRubro(1), MakeRubro(2) }); var handler = new GetRubroTreeQueryHandler(_repo, new NullAvisoQueryRepository()); var result = await handler.Handle(new GetRubroTreeQuery(IncluirInactivos: false)); result.Should().AllSatisfy(n => n.TieneAvisos.Should().BeFalse()); } // ── GetRubroByIdQueryHandler ───────────────────────────────────────────── [Fact] public async Task GetById_Found_Active_ReturnsDto() { _repo.GetByIdAsync(5, Arg.Any()).Returns(MakeRubro(5)); var handler = new GetRubroByIdQueryHandler(_repo); var result = await handler.Handle(new GetRubroByIdQuery(Id: 5)); result.Should().NotBeNull(); result!.Id.Should().Be(5); } [Fact] public async Task GetById_NotFound_ThrowsRubroNotFoundException() { _repo.GetByIdAsync(99, Arg.Any()).Returns((Rubro?)null); var handler = new GetRubroByIdQueryHandler(_repo); var act = () => handler.Handle(new GetRubroByIdQuery(Id: 99)); await act.Should().ThrowAsync(); } }