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

namespace SIGCM2.Application.Tests.Products.Create;

/// <summary>
/// PRD-002 — CreateProductCommandHandler tests.
/// Covers: happy path, flags coherence, duplicate nombre, inactive Medio/ProductType/Rubro,
/// audit, immutability, rollback.
/// </summary>
public class CreateProductCommandHandlerTests
{
    private readonly IProductRepository _repo = Substitute.For<IProductRepository>();
    private readonly IProductTypeRepository _ptRepo = Substitute.For<IProductTypeRepository>();
    private readonly IMedioRepository _medioRepo = Substitute.For<IMedioRepository>();
    private readonly IRubroRepository _rubroRepo = Substitute.For<IRubroRepository>();
    private readonly IAuditLogger _audit = Substitute.For<IAuditLogger>();
    private readonly FakeTimeProvider _time = new(new DateTimeOffset(2026, 4, 19, 10, 0, 0, TimeSpan.Zero));
    private readonly CreateProductCommandHandler _handler;

    private static readonly ProductType _activePtNoFlags = new(
        id: 2, nombre: "Clasificado",
        hasDuration: false, requiresText: false, requiresCategory: false, isBundle: false,
        allowImages: false, maxImages: null, maxImageSizeMB: null, maxImageWidth: null, maxImageHeight: null,
        isActive: true,
        fechaCreacion: new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), fechaModificacion: null);

    private static readonly ProductType _activePtRequiresCategory = new(
        id: 3, nombre: "Con Rubro",
        hasDuration: false, requiresText: false, requiresCategory: true, isBundle: false,
        allowImages: false, maxImages: null, maxImageSizeMB: null, maxImageWidth: null, maxImageHeight: null,
        isActive: true,
        fechaCreacion: new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), fechaModificacion: null);

    private static readonly ProductType _activePtHasDuration = new(
        id: 4, nombre: "Con Duración",
        hasDuration: true, requiresText: false, requiresCategory: false, isBundle: false,
        allowImages: false, maxImages: null, maxImageSizeMB: null, maxImageWidth: null, maxImageHeight: null,
        isActive: true,
        fechaCreacion: new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), fechaModificacion: null);

    private static readonly ProductType _inactivePt = new(
        id: 5, nombre: "Inactivo",
        hasDuration: false, requiresText: false, requiresCategory: false, isBundle: false,
        allowImages: false, maxImages: null, maxImageSizeMB: null, maxImageWidth: null, maxImageHeight: null,
        isActive: false,
        fechaCreacion: new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), fechaModificacion: null);

    private static Medio ActiveMedio(int id = 1) => new(
        id: id, codigo: "ELD", nombre: "El Día",
        tipo: TipoMedio.Diario, plataformaEmpresaId: null,
        activo: true,
        fechaCreacion: new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), fechaModificacion: null);

    private static Medio InactiveMedio(int id = 1) => new(
        id: id, codigo: "ELD", nombre: "El Día",
        tipo: TipoMedio.Diario, plataformaEmpresaId: null,
        activo: false,
        fechaCreacion: new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), fechaModificacion: null);

    private static Rubro ActiveRubro(int id = 10) => new(
        id: id, parentId: null, nombre: "Clasificados", orden: 1,
        activo: true, tarifarioBaseId: null,
        fechaCreacion: new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), fechaModificacion: null);

    private static Rubro InactiveRubro(int id = 10) => new(
        id: id, parentId: null, nombre: "Clasificados", orden: 1,
        activo: false, tarifarioBaseId: null,
        fechaCreacion: new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), fechaModificacion: null);

    public CreateProductCommandHandlerTests()
    {
        _medioRepo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(ActiveMedio(1));
        _ptRepo.GetByIdAsync(2, Arg.Any<CancellationToken>()).Returns(_activePtNoFlags);
        _repo.ExistsByNombreAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<int>(), Arg.Any<int?>(), Arg.Any<CancellationToken>()).Returns(false);
        _repo.AddAsync(Arg.Any<Product>(), Arg.Any<CancellationToken>()).Returns(1);
        _handler = new CreateProductCommandHandler(_repo, _ptRepo, _medioRepo, _rubroRepo, _audit, _time);
    }

    private static CreateProductCommand ValidCmd(int productTypeId = 2) => new(
        Nombre: "Clasificado Estándar",
        MedioId: 1,
        ProductTypeId: productTypeId,
        RubroId: null,
        BasePrice: 100.50m,
        PriceDurationDays: null);

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

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

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

        result.Id.Should().Be(42);
        result.Nombre.Should().Be("Clasificado Estándar");
        result.IsActive.Should().BeTrue();
    }

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

        await _handler.Handle(ValidCmd());

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

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

        await _handler.Handle(ValidCmd());

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

    // ── Medio not found / inactive ────────────────────────────────────────────

    [Fact]
    public async Task Handle_MedioNotFound_ThrowsMedioNotFoundException()
    {
        _medioRepo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns((Medio?)null);

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

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

    [Fact]
    public async Task Handle_MedioInactivo_ThrowsMedioInactivoException()
    {
        _medioRepo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(InactiveMedio(1));

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

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

    // ── ProductType not found / inactive ──────────────────────────────────────

    [Fact]
    public async Task Handle_ProductTypeNotFound_ThrowsProductTypeNotFoundException()
    {
        _ptRepo.GetByIdAsync(2, Arg.Any<CancellationToken>()).Returns((ProductType?)null);

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

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

    [Fact]
    public async Task Handle_ProductTypeInactivo_ThrowsProductTypeInactivoException()
    {
        _ptRepo.GetByIdAsync(5, Arg.Any<CancellationToken>()).Returns(_inactivePt);
        _medioRepo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(ActiveMedio(1));

        var act = async () => await _handler.Handle(ValidCmd(productTypeId: 5));

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

    // ── Flags coherence ───────────────────────────────────────────────────────

    [Fact]
    public async Task Handle_RequiresCategoryTrue_RubroIdNull_ThrowsFlagsException()
    {
        _ptRepo.GetByIdAsync(3, Arg.Any<CancellationToken>()).Returns(_activePtRequiresCategory);
        var cmd = ValidCmd(productTypeId: 3) with { RubroId = null };

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

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

    [Fact]
    public async Task Handle_HasDurationTrue_PriceDurationDaysNull_ThrowsFlagsException()
    {
        _ptRepo.GetByIdAsync(4, Arg.Any<CancellationToken>()).Returns(_activePtHasDuration);
        var cmd = ValidCmd(productTypeId: 4) with { PriceDurationDays = null };

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

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

    // ── Rubro validation ──────────────────────────────────────────────────────

    [Fact]
    public async Task Handle_RubroProvided_RubroInactivo_ThrowsRubroInactivoException()
    {
        _ptRepo.GetByIdAsync(3, Arg.Any<CancellationToken>()).Returns(_activePtRequiresCategory);
        _rubroRepo.GetByIdAsync(10, Arg.Any<CancellationToken>()).Returns(InactiveRubro(10));
        var cmd = ValidCmd(productTypeId: 3) with { RubroId = 10 };

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

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

    // ── Duplicate nombre ──────────────────────────────────────────────────────

    [Fact]
    public async Task Handle_NombreDuplicadoEnMedioTipo_ThrowsProductNombreDuplicadoException()
    {
        _repo.ExistsByNombreAsync("Clasificado Estándar", 1, 2, null, Arg.Any<CancellationToken>()).Returns(true);

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

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

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

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

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

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