using FluentAssertions;
using Microsoft.Extensions.Time.Testing;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using SIGCM2.Application.Abstractions.Persistence;
using SIGCM2.Application.Audit;
using SIGCM2.Application.Products.Update;
using SIGCM2.Domain.Entities;
using SIGCM2.Domain.Exceptions;
namespace SIGCM2.Application.Tests.Products.Update;
///
/// PRD-002 — UpdateProductCommandHandler tests.
///
public class UpdateProductCommandHandlerTests
{
private readonly IProductRepository _repo = Substitute.For();
private readonly IProductTypeRepository _ptRepo = Substitute.For();
private readonly IRubroRepository _rubroRepo = Substitute.For();
private readonly IAuditLogger _audit = Substitute.For();
private readonly FakeTimeProvider _time = new(new DateTimeOffset(2026, 4, 19, 10, 0, 0, TimeSpan.Zero));
private readonly UpdateProductCommandHandler _handler;
private static readonly ProductType _activePtNoFlags = new(
id: 2, nombre: "Clasificado",
hasDuration: false, requiresText: false, requiresCategory: false, isBundle: false,
allowImages: false, maxImages: null, maxImageSizeMB: null, maxImageWidth: null, maxImageHeight: null,
isActive: true,
fechaCreacion: new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), fechaModificacion: null);
private static readonly ProductType _activePtRequiresCategory = new(
id: 3, nombre: "Con Rubro",
hasDuration: false, requiresText: false, requiresCategory: true, isBundle: false,
allowImages: false, maxImages: null, maxImageSizeMB: null, maxImageWidth: null, maxImageHeight: null,
isActive: true,
fechaCreacion: new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), fechaModificacion: null);
private static Product AProduct(int id = 1, int productTypeId = 2) => new(
id: id, nombre: "Clasificado Estándar",
medioId: 1, productTypeId: productTypeId, rubroId: null,
basePrice: 100.50m, priceDurationDays: null,
isActive: true,
fechaCreacion: new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc),
fechaModificacion: null);
public UpdateProductCommandHandlerTests()
{
_repo.GetByIdAsync(1, Arg.Any()).Returns(AProduct(1));
_ptRepo.GetByIdAsync(2, Arg.Any()).Returns(_activePtNoFlags);
_repo.ExistsByNombreAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()).Returns(false);
_handler = new UpdateProductCommandHandler(_repo, _ptRepo, _rubroRepo, _audit, _time);
}
private static UpdateProductCommand ValidCmd() => new(
Id: 1,
Nombre: "Nuevo Nombre",
RubroId: null,
BasePrice: 200m,
PriceDurationDays: null);
// ── Happy path ───────────────────────────────────────────────────────────
[Fact]
public async Task Handle_ValidCommand_UpdatesAndReturnsDto()
{
var result = await _handler.Handle(ValidCmd());
result.Id.Should().Be(1);
result.Nombre.Should().Be("Nuevo Nombre");
result.BasePrice.Should().Be(200m);
}
[Fact]
public async Task Handle_ValidCommand_CallsUpdateAsync()
{
await _handler.Handle(ValidCmd());
await _repo.Received(1).UpdateAsync(
Arg.Is(p => p.Nombre == "Nuevo Nombre"),
Arg.Any());
}
[Fact]
public async Task Handle_ValidCommand_LogsAuditEvent_ProductoUpdated()
{
await _handler.Handle(ValidCmd());
await _audit.Received(1).LogAsync(
action: "producto.updated",
targetType: "Product",
targetId: "1",
metadata: Arg.Any