using FluentAssertions;
using Microsoft.Extensions.Time.Testing;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using SIGCM2.Application.Abstractions.Persistence;
using SIGCM2.Application.Audit;
using SIGCM2.Application.Common;
using SIGCM2.Application.Products.Prices;
using SIGCM2.Application.Products.Prices.AddPrice;
using SIGCM2.Domain.Entities;
using SIGCM2.Domain.Exceptions;
namespace SIGCM2.Application.Tests.Products.Prices;
///
/// PRD-003 — AddProductPriceCommandHandler tests.
/// Covers: §REQ-6.1 (happy path + audit), §REQ-6.2 (audit fail → rollback),
/// §REQ-3.3 (producto inexistente → ProductNotFoundException),
/// §REQ-2.2 (ForwardOnly propagation), §REQ-2.1 (response con Closed).
/// NSubstitute, FakeTimeProvider.
///
public class AddProductPriceCommandHandlerTests
{
private readonly IProductPriceRepository _pricesRepo = Substitute.For();
private readonly IProductRepository _productsRepo = Substitute.For();
private readonly IAuditLogger _audit = Substitute.For();
private readonly FakeTimeProvider _time = new(new DateTimeOffset(2026, 4, 19, 12, 0, 0, TimeSpan.Zero));
private readonly AddProductPriceCommandHandler _handler;
private static readonly DateOnly Today = new(2026, 4, 19);
private static readonly DateOnly Tomorrow = new(2026, 4, 20);
// Producto activo de ejemplo
private static Product ActiveProduct(int id = 1) => Product.ForCreation(
"Test Product", medioId: 1, productTypeId: 2,
rubroId: null, basePrice: 100m, priceDurationDays: null,
TimeProvider.System);
// ProductPrice stub de retorno
private static ProductPrice MakePrice(long id, int productId, decimal price,
DateOnly pvf, DateOnly? pvt = null) =>
new(id, productId, price, pvf, pvt,
FechaCreacion: new DateTime(2026, 4, 19, 0, 0, 0, DateTimeKind.Utc));
public AddProductPriceCommandHandlerTests()
{
// defaults: producto activo existe; AddAsync devuelve newId=10, closedId=null
_productsRepo.GetByIdAsync(1, Arg.Any())
.Returns(ActiveProduct(1));
_pricesRepo.AddAsync(1, Arg.Any(), Arg.Any(), Arg.Any())
.Returns((10L, (long?)null));
_pricesRepo.GetByProductIdAsync(1, Arg.Any(), Arg.Any(), Arg.Any())
.Returns(new PagedResult(
new List { MakePrice(10, 1, 150m, Today) },
1, 2, 1));
_handler = new AddProductPriceCommandHandler(_pricesRepo, _productsRepo, _audit, _time);
}
private static AddProductPriceCommand ValidCmd(int productId = 1) => new(
ProductId: productId,
Price: 150m,
PriceValidFrom: Today);
// ── Happy path ─────────────────────────────────────────────────────────────
[Fact]
public async Task Handle_HappyPath_ReturnsCreatedDto()
{
// §REQ-1.1 happy path — primer precio (sin activo previo)
var result = await _handler.Handle(ValidCmd());
result.Created.Should().NotBeNull();
result.Created.Id.Should().Be(10);
result.Created.Price.Should().Be(150m);
result.Created.PriceValidFrom.Should().Be(Today);
result.Created.IsActive.Should().BeTrue();
result.Closed.Should().BeNull();
}
[Fact]
public async Task Handle_HappyPath_CallsRepoAddAsync_Once()
{
await _handler.Handle(ValidCmd());
await _pricesRepo.Received(1).AddAsync(
1, 150m, Today, Arg.Any());
}
[Fact]
public async Task Handle_HappyPath_LogsAuditEvent_ProductPriceCreated()
{
// §REQ-6.1 — audit "product_price.created" llamado exactamente una vez
await _handler.Handle(ValidCmd());
await _audit.Received(1).LogAsync(
action: "product_price.created",
targetType: "ProductPrice",
targetId: "10",
metadata: Arg.Any