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

namespace SIGCM2.Application.Tests.ProductTypes.Create;

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

    public CreateProductTypeCommandHandlerTests()
    {
        _repo.ExistsByNombreAsync(Arg.Any<string>(), Arg.Any<int?>(), Arg.Any<CancellationToken>()).Returns(false);
        _repo.AddAsync(Arg.Any<ProductType>(), Arg.Any<CancellationToken>()).Returns(1);
        _handler = new CreateProductTypeCommandHandler(_repo, _audit, _timeProvider);
    }

    private static CreateProductTypeCommand ValidCommand(bool allowImages = false) => new(
        Nombre: "Clasificados",
        HasDuration: true, RequiresText: true, RequiresCategory: false, IsBundle: false,
        AllowImages: allowImages,
        MaxImages: null, MaxImageSizeMB: null, MaxImageWidth: null, MaxImageHeight: null);

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

    [Fact]
    public async Task Handle_ValidCommand_InsertsAndReturnsDto()
    {
        _repo.AddAsync(Arg.Any<ProductType>(), Arg.Any<CancellationToken>()).Returns(42);

        var result = await _handler.Handle(ValidCommand());

        result.Id.Should().Be(42);
        result.Nombre.Should().Be("Clasificados");
    }

    [Fact]
    public async Task Handle_ValidCommand_CallsAddAsync()
    {
        await _handler.Handle(ValidCommand());

        await _repo.Received(1).AddAsync(
            Arg.Is<ProductType>(pt => pt.Nombre == "Clasificados"),
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task Handle_SuccessfulInsert_LogsAuditEventProductoTipoCreated()
    {
        _repo.AddAsync(Arg.Any<ProductType>(), Arg.Any<CancellationToken>()).Returns(7);

        await _handler.Handle(ValidCommand());

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

    [Fact]
    public async Task Handle_PreservesFlagsAsProvided()
    {
        var cmd = ValidCommand() with { HasDuration = true, IsBundle = true };

        await _handler.Handle(cmd);

        await _repo.Received(1).AddAsync(
            Arg.Is<ProductType>(pt => pt.HasDuration && pt.IsBundle),
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task Handle_AllowImagesTrue_WithAllMaxNull_PersistsAllNull()
    {
        var cmd = ValidCommand(allowImages: true);

        await _handler.Handle(cmd);

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

    [Fact]
    public async Task Handle_UsesTimeProvider_NotDateTimeNow()
    {
        var expectedDate = _timeProvider.GetUtcNow().UtcDateTime;

        await _handler.Handle(ValidCommand());

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

    [Fact]
    public async Task Handle_ReturnsCreatedDtoWithPersistedId()
    {
        _repo.AddAsync(Arg.Any<ProductType>(), Arg.Any<CancellationToken>()).Returns(99);

        var result = await _handler.Handle(ValidCommand());

        result.Id.Should().Be(99);
        result.IsActive.Should().BeTrue();
    }

    // ── AllowImages=false normalization ───────────────────────────────────────

    [Fact]
    public async Task Handle_AllowImagesFalse_MaxImagesInput5_PersistsWithMaxImagesNull()
    {
        var cmd = ValidCommand() with { AllowImages = false, MaxImages = 5 };

        await _handler.Handle(cmd);

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

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

    [Fact]
    public async Task Handle_NombreDuplicado_ThrowsProductTypeNombreDuplicadoException()
    {
        _repo.ExistsByNombreAsync("Clasificados", Arg.Any<int?>(), Arg.Any<CancellationToken>()).Returns(true);

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

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

    [Fact]
    public async Task Handle_NombreDuplicado_CheckedBeforeFactory()
    {
        // If duplicate check throws, AddAsync should never be called
        _repo.ExistsByNombreAsync(Arg.Any<string>(), Arg.Any<int?>(), Arg.Any<CancellationToken>()).Returns(true);

        try { await _handler.Handle(ValidCommand()); } catch (ProductTypeNombreDuplicadoException) { }

        await _repo.DidNotReceive().AddAsync(Arg.Any<ProductType>(), Arg.Any<CancellationToken>());
    }

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

    [Fact]
    public async Task Handle_RepoThrows_AuditNotCalled_TransactionRollback()
    {
        _repo.AddAsync(Arg.Any<ProductType>(), Arg.Any<CancellationToken>())
             .ThrowsAsync(new InvalidOperationException("DB error"));

        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_TransactionRollback()
    {
        _audit.LogAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
            Arg.Any<object?>(), Arg.Any<CancellationToken>())
            .ThrowsAsync(new InvalidOperationException("Audit error"));

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

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