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.Deactivate;
using SIGCM2.Domain.Entities;
using SIGCM2.Domain.Exceptions;

namespace SIGCM2.Application.Tests.Products.Deactivate;

/// <summary>
/// PRD-002 — DeactivateProductCommandHandler tests.
/// </summary>
public class DeactivateProductCommandHandlerTests
{
    private readonly IProductRepository _repo = Substitute.For<IProductRepository>();
    private readonly IAuditLogger _audit = Substitute.For<IAuditLogger>();
    private readonly FakeTimeProvider _time = new(new DateTimeOffset(2026, 4, 19, 14, 0, 0, TimeSpan.Zero));
    private readonly DeactivateProductCommandHandler _handler;

    public DeactivateProductCommandHandlerTests()
    {
        _handler = new DeactivateProductCommandHandler(_repo, _audit, _time);
    }

    private static Product ActiveProduct(int id = 1) => new(
        id: id, nombre: "Clasificado Estándar",
        medioId: 1, productTypeId: 2, rubroId: null,
        basePrice: 100.50m, priceDurationDays: null,
        isActive: true,
        fechaCreacion: new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc),
        fechaModificacion: null);

    private static Product InactiveProduct(int id = 1) => new(
        id: id, nombre: "Clasificado Estándar",
        medioId: 1, productTypeId: 2, rubroId: null,
        basePrice: 100.50m, priceDurationDays: null,
        isActive: false,
        fechaCreacion: new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc),
        fechaModificacion: null);

    // ── Not found ────────────────────────────────────────────────────────────

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

        var act = async () => await _handler.Handle(new DeactivateProductCommand(99));

        await act.Should().ThrowAsync<ProductNotFoundException>()
            .Where(e => e.ProductId == 99);
    }

    // ── Already inactive (idempotent) ─────────────────────────────────────────

    [Fact]
    public async Task Handle_AlreadyInactive_ReturnsDto_NoAudit_NoRepoUpdate()
    {
        _repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(InactiveProduct());

        var result = await _handler.Handle(new DeactivateProductCommand(1));

        result.Id.Should().Be(1);
        result.IsActive.Should().BeFalse();
        await _repo.DidNotReceive().UpdateAsync(Arg.Any<Product>(), Arg.Any<CancellationToken>());
        await _audit.DidNotReceive().LogAsync(
            Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
            Arg.Any<object?>(), Arg.Any<CancellationToken>());
    }

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

    [Fact]
    public async Task Handle_ActiveProduct_DeactivatesAndAudits()
    {
        _repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(ActiveProduct());

        await _handler.Handle(new DeactivateProductCommand(1));

        await _repo.Received(1).UpdateAsync(
            Arg.Is<Product>(p => !p.IsActive),
            Arg.Any<CancellationToken>());
        await _audit.Received(1).LogAsync(
            action: "producto.deactivated",
            targetType: "Product",
            targetId: "1",
            metadata: Arg.Any<object?>(),
            ct: Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task Handle_UsesTimeProviderInDeactivate()
    {
        _repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(ActiveProduct());
        var expectedDate = _time.GetUtcNow().UtcDateTime;

        await _handler.Handle(new DeactivateProductCommand(1));

        await _repo.Received(1).UpdateAsync(
            Arg.Is<Product>(p => p.FechaModificacion == expectedDate),
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task Handle_ReturnsDtoWithIsActiveFalse()
    {
        _repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(ActiveProduct());

        var result = await _handler.Handle(new DeactivateProductCommand(1));

        result.IsActive.Should().BeFalse();
    }

    // ── Rollback ──────────────────────────────────────────────────────────────

    [Fact]
    public async Task Handle_RepoThrows_AuditNotCalled()
    {
        _repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(ActiveProduct());
        _repo.UpdateAsync(Arg.Any<Product>(), Arg.Any<CancellationToken>())
             .ThrowsAsync(new InvalidOperationException("DB error"));

        var act = async () => await _handler.Handle(new DeactivateProductCommand(1));
        await act.Should().ThrowAsync<InvalidOperationException>();

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