using FluentAssertions;
using NSubstitute;
using SIGCM2.Application.Abstractions.Persistence;
using SIGCM2.Application.Products.Prices;
using SIGCM2.Application.Products.Prices.GetHistory;
using SIGCM2.Domain.Entities;
using SIGCM2.Domain.Exceptions;

namespace SIGCM2.Application.Tests.Products.Prices;

/// <summary>
/// PRD-003 — GetProductPricesQueryHandler tests.
/// Covers: §REQ-4.1 (historial descending), §REQ-4.3 (lista vacía), §REQ-3.3 (producto no existe → 404).
/// </summary>
public class GetProductPricesQueryHandlerTests
{
    private readonly IProductPriceRepository _pricesRepo = Substitute.For<IProductPriceRepository>();
    private readonly IProductRepository _productsRepo = Substitute.For<IProductRepository>();
    private readonly GetProductPricesQueryHandler _handler;

    private static readonly DateOnly Date1 = new(2026, 1, 1);
    private static readonly DateOnly Date2 = new(2026, 2, 1);
    private static readonly DateOnly Date3 = new(2026, 3, 1);

    private static Product ActiveProduct() => Product.ForCreation(
        "Test", medioId: 1, productTypeId: 2,
        rubroId: null, basePrice: 100m, priceDurationDays: null,
        TimeProvider.System);

    private static ProductPrice MakePrice(long id, DateOnly pvf, DateOnly? pvt = null) =>
        new(id, ProductId: 1, Price: 100m * id, pvf, pvt,
            FechaCreacion: new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc));

    public GetProductPricesQueryHandlerTests()
    {
        _productsRepo.GetByIdAsync(1, Arg.Any<CancellationToken>())
            .Returns(ActiveProduct());

        // default: lista con 2 precios, el repo ya los devuelve descending (responsabilidad del repo)
        _pricesRepo.GetByProductIdAsync(1, Arg.Any<CancellationToken>())
            .Returns(new List<ProductPrice>
            {
                MakePrice(3, Date3),          // activo (pvt=null)
                MakePrice(2, Date2, Date3.AddDays(-1)), // cerrado
                MakePrice(1, Date1, Date2.AddDays(-1))  // cerrado más antiguo
            }.AsReadOnly() as IReadOnlyList<ProductPrice>);

        _handler = new GetProductPricesQueryHandler(_pricesRepo, _productsRepo);
    }

    // ── Orden descending ────────────────────────────────────────────────────────

    [Fact]
    public async Task Handle_ReturnsAllPrices_InDescendingOrder()
    {
        // §REQ-4.1 — historial completo ordenado descending por PriceValidFrom
        var result = await _handler.Handle(new GetProductPricesQuery(ProductId: 1));

        result.Should().HaveCount(3);
        result[0].PriceValidFrom.Should().Be(Date3);  // más reciente primero
        result[1].PriceValidFrom.Should().Be(Date2);
        result[2].PriceValidFrom.Should().Be(Date1);
    }

    [Fact]
    public async Task Handle_MapsToDto_WithIsActive()
    {
        var result = await _handler.Handle(new GetProductPricesQuery(ProductId: 1));

        result[0].IsActive.Should().BeTrue();   // pvt=null → activo
        result[1].IsActive.Should().BeFalse();  // pvt IS NOT NULL → cerrado
    }

    // ── Lista vacía (nuevo producto sin precios) ─────────────────────────────────

    [Fact]
    public async Task Handle_EmptyHistory_ReturnsEmptyList()
    {
        // §REQ-4.3 — nuevo producto aún no tiene precios → lista vacía (no 404)
        _pricesRepo.GetByProductIdAsync(1, Arg.Any<CancellationToken>())
            .Returns(new List<ProductPrice>().AsReadOnly() as IReadOnlyList<ProductPrice>);

        var result = await _handler.Handle(new GetProductPricesQuery(ProductId: 1));

        result.Should().BeEmpty();
    }

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

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

        var act = async () => await _handler.Handle(new GetProductPricesQuery(ProductId: 99));

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

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

        var act = async () => await _handler.Handle(new GetProductPricesQuery(ProductId: 99));
        await act.Should().ThrowAsync<ProductNotFoundException>();

        await _pricesRepo.DidNotReceive()
            .GetByProductIdAsync(99, Arg.Any<CancellationToken>());
    }
}
