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.Prices;
using SIGCM2.Application.Products.Prices.AddPrice;
using SIGCM2.Domain.Entities;
using SIGCM2.Domain.Exceptions;

namespace SIGCM2.Application.Tests.Products.Prices;

/// <summary>
/// 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.
/// </summary>
public class AddProductPriceCommandHandlerTests
{
    private readonly IProductPriceRepository _pricesRepo = Substitute.For<IProductPriceRepository>();
    private readonly IProductRepository _productsRepo = Substitute.For<IProductRepository>();
    private readonly IAuditLogger _audit = Substitute.For<IAuditLogger>();
    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<CancellationToken>())
            .Returns(ActiveProduct(1));

        _pricesRepo.AddAsync(1, Arg.Any<decimal>(), Arg.Any<DateOnly>(), Arg.Any<CancellationToken>())
            .Returns((10L, (long?)null));

        _pricesRepo.GetByProductIdAsync(1, Arg.Any<CancellationToken>())
            .Returns(new List<ProductPrice>
            {
                MakePrice(10, 1, 150m, Today)
            }.AsReadOnly() as IReadOnlyList<ProductPrice>);

        _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<CancellationToken>());
    }

    [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<object?>(),
            ct: Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task Handle_ClosesActivePrice_ResponseContainsClosed()
    {
        // §REQ-2.1 — POST crea nuevo y cierra el activo → response.Closed != null
        _pricesRepo.AddAsync(1, Arg.Any<decimal>(), Arg.Any<DateOnly>(), Arg.Any<CancellationToken>())
            .Returns((20L, (long?)5L)); // newId=20, closedId=5

        _pricesRepo.GetByProductIdAsync(1, Arg.Any<CancellationToken>())
            .Returns(new List<ProductPrice>
            {
                MakePrice(20, 1, 200m, Tomorrow),
                MakePrice(5, 1, 150m, Today, pvt: Tomorrow.AddDays(-1))
            }.AsReadOnly() as IReadOnlyList<ProductPrice>);

        var result = await _handler.Handle(ValidCmd() with { Price = 200m, PriceValidFrom = Tomorrow });

        result.Created.Id.Should().Be(20);
        result.Closed.Should().NotBeNull();
        result.Closed!.Id.Should().Be(5);
        result.Closed.IsActive.Should().BeFalse();
    }

    // ── Producto inexistente ────────────────────────────────────────────────────

    [Fact]
    public async Task Handle_ProductNotFound_ThrowsProductNotFoundException()
    {
        // §REQ-3.3
        _productsRepo.GetByIdAsync(99, Arg.Any<CancellationToken>())
            .Returns((Product?)null);

        var act = async () => await _handler.Handle(ValidCmd(productId: 99));

        await act.Should().ThrowAsync<ProductNotFoundException>();
    }

    [Fact]
    public async Task Handle_ProductNotFound_RepoAddAsync_NotCalled()
    {
        _productsRepo.GetByIdAsync(99, Arg.Any<CancellationToken>())
            .Returns((Product?)null);

        var act = async () => await _handler.Handle(ValidCmd(productId: 99));
        await act.Should().ThrowAsync<ProductNotFoundException>();

        await _pricesRepo.DidNotReceive().AddAsync(
            Arg.Any<int>(), Arg.Any<decimal>(), Arg.Any<DateOnly>(), Arg.Any<CancellationToken>());
    }

    // ── Producto inactivo ───────────────────────────────────────────────────────

    [Fact]
    public async Task Handle_ProductInactive_ThrowsProductNotFoundException()
    {
        // Producto inactivo → tratamos como 404 (invisible para clientes)
        var activeFirst = Product.ForCreation(
            "Inactive", medioId: 1, productTypeId: 2,
            rubroId: null, basePrice: 100m, priceDurationDays: null,
            TimeProvider.System);
        var inactiveProduct = activeFirst.WithDeactivated(TimeProvider.System);

        _productsRepo.GetByIdAsync(2, Arg.Any<CancellationToken>())
            .Returns(inactiveProduct);

        var act = async () => await _handler.Handle(ValidCmd(productId: 2));

        await act.Should().ThrowAsync<ProductNotFoundException>();
    }

    // ── ForwardOnly violation ───────────────────────────────────────────────────

    [Fact]
    public async Task Handle_RepoThrowsForwardOnlyException_PropagatesIt()
    {
        // §REQ-2.3 — SP lanza 50409 → repo lo mapea a ProductPriceForwardOnlyException → handler lo propaga
        _pricesRepo.AddAsync(1, Arg.Any<decimal>(), Arg.Any<DateOnly>(), Arg.Any<CancellationToken>())
            .ThrowsAsync(new ProductPriceForwardOnlyException(1, Today, Today));

        var act = async () => await _handler.Handle(ValidCmd());

        await act.Should().ThrowAsync<ProductPriceForwardOnlyException>();
    }

    [Fact]
    public async Task Handle_RepoThrowsForwardOnlyException_AuditNotCalled()
    {
        _pricesRepo.AddAsync(1, Arg.Any<decimal>(), Arg.Any<DateOnly>(), Arg.Any<CancellationToken>())
            .ThrowsAsync(new ProductPriceForwardOnlyException(1, Today, Today));

        var act = async () => await _handler.Handle(ValidCmd());
        await act.Should().ThrowAsync<ProductPriceForwardOnlyException>();

        await _audit.DidNotReceive().LogAsync(
            Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
            Arg.Any<object?>(), Arg.Any<CancellationToken>());
    }

    // ── Audit fail → rollback ───────────────────────────────────────────────────

    [Fact]
    public async Task Handle_AuditThrows_ExceptionPropagates_TransactionNotCompleted()
    {
        // §REQ-6.2 — audit falla → rollback total (no commit)
        _audit.LogAsync(
            action: Arg.Any<string>(),
            targetType: Arg.Any<string>(),
            targetId: Arg.Any<string>(),
            metadata: Arg.Any<object?>(),
            ct: Arg.Any<CancellationToken>())
            .ThrowsAsync(new InvalidOperationException("Audit DB error"));

        var act = async () => await _handler.Handle(ValidCmd());

        await act.Should().ThrowAsync<InvalidOperationException>()
            .WithMessage("Audit DB error");
    }
}
