using FluentAssertions;
using Microsoft.Extensions.Time.Testing;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using SIGCM2.Application.Abstractions.Persistence;
using SIGCM2.Application.Audit;
using SIGCM2.Application.ProductTypes.Update;
using SIGCM2.Domain.Entities;
using SIGCM2.Domain.Exceptions;

namespace SIGCM2.Application.Tests.ProductTypes.Update;

public class UpdateProductTypeCommandHandlerTests
{
    private readonly IProductTypeRepository _repo = Substitute.For<IProductTypeRepository>();
    private readonly IAuditLogger _audit = Substitute.For<IAuditLogger>();
    private readonly FakeTimeProvider _timeProvider = new(new DateTimeOffset(2026, 4, 19, 12, 0, 0, TimeSpan.Zero));
    private readonly UpdateProductTypeCommandHandler _handler;

    public UpdateProductTypeCommandHandlerTests()
    {
        _repo.ExistsByNombreAsync(Arg.Any<string>(), Arg.Any<int?>(), Arg.Any<CancellationToken>()).Returns(false);
        _handler = new UpdateProductTypeCommandHandler(_repo, _audit, _timeProvider);
    }

    private static ProductType ExistingType(int id = 1, string nombre = "Clasificados", bool isActive = true) =>
        new(id, nombre, false, false, false, false, false, null, null, null, null,
            isActive, new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), null);

    private static UpdateProductTypeCommand ValidCommand(int id = 1, string nombre = "Clasificados Actualizado") => new(
        Id: id,
        Nombre: nombre,
        HasDuration: true, RequiresText: false, RequiresCategory: true, IsBundle: false,
        AllowImages: false,
        MaxImages: null, MaxImageSizeMB: null, MaxImageWidth: null, MaxImageHeight: null);

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

    [Fact]
    public async Task Handle_NotFound_ThrowsProductTypeNotFoundException()
    {
        _repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns((ProductType?)null);

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

        await act.Should().ThrowAsync<ProductTypeNotFoundException>()
            .Where(e => e.ProductTypeId == 1);
    }

    // ── Nombre duplicado ─────────────────────────────────────────────────────

    [Fact]
    public async Task Handle_RenameToSameName_DoesNotCheckDuplicate()
    {
        // Same nombre → no duplicate check needed
        _repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(ExistingType(nombre: "Clasificados"));

        await _handler.Handle(ValidCommand(nombre: "Clasificados"));

        await _repo.DidNotReceive().ExistsByNombreAsync(
            Arg.Any<string>(), Arg.Any<int?>(), Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task Handle_RenameToExistingActiveName_ThrowsProductTypeNombreDuplicadoException()
    {
        _repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(ExistingType(nombre: "Clasificados"));
        _repo.ExistsByNombreAsync("Notables", 1, Arg.Any<CancellationToken>()).Returns(true);

        var act = async () => await _handler.Handle(ValidCommand(nombre: "Notables"));

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

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

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

        await _handler.Handle(ValidCommand());

        await _repo.Received(1).UpdateAsync(Arg.Any<ProductType>(), Arg.Any<CancellationToken>());
        await _audit.Received(1).LogAsync(
            action: "producto_tipo.updated",
            targetType: "ProductType",
            targetId: "1",
            metadata: Arg.Any<object?>(),
            ct: Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task Handle_TurnOffAllowImages_NullifiesMultimedia()
    {
        var existing = new ProductType(
            1, "Tipo", false, false, false, false,
            allowImages: true, maxImages: 5, maxImageSizeMB: 2.0m, maxImageWidth: 800, maxImageHeight: 600,
            isActive: true, DateTime.UtcNow, null);
        _repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(existing);

        var cmd = ValidCommand() with { AllowImages = false };
        await _handler.Handle(cmd);

        await _repo.Received(1).UpdateAsync(
            Arg.Is<ProductType>(pt => !pt.AllowImages && pt.MaxImages == null),
            Arg.Any<CancellationToken>());
    }

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

        await _handler.Handle(ValidCommand());

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

    [Fact]
    public async Task Handle_PreservesIsActiveFromTarget()
    {
        _repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(ExistingType(isActive: true));

        await _handler.Handle(ValidCommand());

        await _repo.Received(1).UpdateAsync(
            Arg.Is<ProductType>(pt => pt.IsActive),
            Arg.Any<CancellationToken>());
    }

    // ── Rollback scenarios ────────────────────────────────────────────────────

    [Fact]
    public async Task Handle_RepoThrows_NoAudit_Rollback()
    {
        _repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(ExistingType());
        _repo.UpdateAsync(Arg.Any<ProductType>(), Arg.Any<CancellationToken>())
             .ThrowsAsync(new InvalidOperationException("DB"));

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

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

    [Fact]
    public async Task Handle_AuditThrows_Rollback()
    {
        _repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(ExistingType());
        _audit.LogAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
            Arg.Any<object?>(), Arg.Any<CancellationToken>())
            .ThrowsAsync(new InvalidOperationException("Audit"));

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

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

    [Fact]
    public async Task Handle_AuditLoggerLogsBeforeAndAfter()
    {
        _repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(ExistingType(nombre: "Antes"));

        await _handler.Handle(ValidCommand(nombre: "Despues"));

        await _audit.Received(1).LogAsync(
            action: "producto_tipo.updated",
            targetType: "ProductType",
            targetId: "1",
            metadata: Arg.Any<object?>(),
            ct: Arg.Any<CancellationToken>());
    }
}
