using FluentAssertions;
using NSubstitute;
using SIGCM2.Application.Abstractions.Persistence;
using SIGCM2.Application.Products.Pricing;
using SIGCM2.Domain.Entities;

namespace SIGCM2.Application.Tests.Products.Pricing;

/// <summary>
/// PRD-003 — ProductPricingService tests.
/// Covers: GetPriceAtAsync happy path, fecha sin precio → null (OQ-B contrato).
/// </summary>
public class ProductPricingServiceTests
{
    private readonly IProductPriceRepository _repo = Substitute.For<IProductPriceRepository>();
    private readonly ProductPricingService _service;

    private static readonly DateOnly QueryDate = new(2026, 4, 19);

    private static ProductPrice ActivePrice(decimal price = 150m) =>
        new(Id: 1, ProductId: 1, Price: price,
            PriceValidFrom: new DateOnly(2026, 1, 1), PriceValidTo: null,
            FechaCreacion: new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc));

    public ProductPricingServiceTests()
    {
        _service = new ProductPricingService(_repo);
    }

    // ── Happy path ─────────────────────────────────────────────────────────────

    [Fact]
    public async Task GetPriceAtAsync_ActivePriceCoverDate_ReturnsPrice()
    {
        _repo.GetActiveAsync(1, QueryDate, Arg.Any<CancellationToken>())
            .Returns(ActivePrice(200m));

        var result = await _service.GetPriceAtAsync(1, QueryDate);

        result.Should().Be(200m);
    }

    [Fact]
    public async Task GetPriceAtAsync_NoPriceForDate_ReturnsNull()
    {
        // OQ-B — si no hay historial para la fecha, retorna null.
        // El consumidor (PRC-001) decide si usar BasePrice o lanzar excepción.
        _repo.GetActiveAsync(1, QueryDate, Arg.Any<CancellationToken>())
            .Returns((ProductPrice?)null);

        var result = await _service.GetPriceAtAsync(1, QueryDate);

        result.Should().BeNull();
    }

    [Fact]
    public async Task GetPriceAtAsync_CallsGetActiveAsync_WithCorrectArgs()
    {
        _repo.GetActiveAsync(Arg.Any<int>(), Arg.Any<DateOnly>(), Arg.Any<CancellationToken>())
            .Returns((ProductPrice?)null);

        await _service.GetPriceAtAsync(productId: 5, date: QueryDate);

        await _repo.Received(1).GetActiveAsync(5, QueryDate, Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task GetPriceAtAsync_FutureDateOutsideWindow_ReturnsNull()
    {
        // Precio cerrado cuya ventana no cubre la fecha consultada → repo devuelve null
        var futureDate = new DateOnly(2030, 1, 1);
        _repo.GetActiveAsync(1, futureDate, Arg.Any<CancellationToken>())
            .Returns((ProductPrice?)null);

        var result = await _service.GetPriceAtAsync(1, futureDate);

        result.Should().BeNull();
    }
}
