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.Create;
using SIGCM2.Domain.Entities;
using SIGCM2.Domain.Exceptions;
namespace SIGCM2.Application.Tests.Products.Create;
///
/// PRD-002 — CreateProductCommandHandler tests.
/// Covers: happy path, flags coherence, duplicate nombre, inactive Medio/ProductType/Rubro,
/// audit, immutability, rollback.
///
public class CreateProductCommandHandlerTests
{
private readonly IProductRepository _repo = Substitute.For();
private readonly IProductTypeRepository _ptRepo = Substitute.For();
private readonly IMedioRepository _medioRepo = 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 CreateProductCommandHandler _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 readonly ProductType _activePtHasDuration = new(
id: 4, nombre: "Con Duración",
hasDuration: true, 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 _inactivePt = new(
id: 5, nombre: "Inactivo",
hasDuration: false, requiresText: false, requiresCategory: false, isBundle: false,
allowImages: false, maxImages: null, maxImageSizeMB: null, maxImageWidth: null, maxImageHeight: null,
isActive: false,
fechaCreacion: new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), fechaModificacion: null);
private static Medio ActiveMedio(int id = 1) => new(
id: id, codigo: "ELD", nombre: "El Día",
tipo: TipoMedio.Diario, plataformaEmpresaId: null,
activo: true,
fechaCreacion: new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), fechaModificacion: null);
private static Medio InactiveMedio(int id = 1) => new(
id: id, codigo: "ELD", nombre: "El Día",
tipo: TipoMedio.Diario, plataformaEmpresaId: null,
activo: false,
fechaCreacion: new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), fechaModificacion: null);
private static Rubro ActiveRubro(int id = 10) => new(
id: id, parentId: null, nombre: "Clasificados", orden: 1,
activo: true, tarifarioBaseId: null,
fechaCreacion: new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), fechaModificacion: null);
private static Rubro InactiveRubro(int id = 10) => new(
id: id, parentId: null, nombre: "Clasificados", orden: 1,
activo: false, tarifarioBaseId: null,
fechaCreacion: new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), fechaModificacion: null);
public CreateProductCommandHandlerTests()
{
_medioRepo.GetByIdAsync(1, Arg.Any()).Returns(ActiveMedio(1));
_ptRepo.GetByIdAsync(2, Arg.Any()).Returns(_activePtNoFlags);
_repo.ExistsByNombreAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()).Returns(false);
_repo.AddAsync(Arg.Any(), Arg.Any()).Returns(1);
_handler = new CreateProductCommandHandler(_repo, _ptRepo, _medioRepo, _rubroRepo, _audit, _time);
}
private static CreateProductCommand ValidCmd(int productTypeId = 2) => new(
Nombre: "Clasificado Estándar",
MedioId: 1,
ProductTypeId: productTypeId,
RubroId: null,
BasePrice: 100.50m,
PriceDurationDays: null);
// ── Happy path ───────────────────────────────────────────────────────────
[Fact]
public async Task Handle_ValidCommand_InsertsAndReturnsDto()
{
_repo.AddAsync(Arg.Any(), Arg.Any()).Returns(42);
var result = await _handler.Handle(ValidCmd());
result.Id.Should().Be(42);
result.Nombre.Should().Be("Clasificado Estándar");
result.IsActive.Should().BeTrue();
}
[Fact]
public async Task Handle_ValidCommand_LogsAuditEvent_ProductoCreated()
{
_repo.AddAsync(Arg.Any(), Arg.Any()).Returns(7);
await _handler.Handle(ValidCmd());
await _audit.Received(1).LogAsync(
action: "producto.created",
targetType: "Product",
targetId: "7",
metadata: Arg.Any