using NSubstitute;
using SIGCM2.Application.Abstractions.Persistence;
using SIGCM2.Application.Audit;
using SIGCM2.Application.TiposDeIva.NuevaVersion;
using SIGCM2.Domain.Entities;
using SIGCM2.Domain.Exceptions;

namespace SIGCM2.Application.Tests.TiposDeIva.NuevaVersion;

public class NuevaVersionTipoDeIvaCommandHandlerTests
{
    private readonly ITipoDeIvaRepository _repo = Substitute.For<ITipoDeIvaRepository>();
    private readonly IAuditLogger _audit = Substitute.For<IAuditLogger>();
    private readonly NuevaVersionTipoDeIvaCommandHandler _handler;

    private static TipoDeIva MakePredecesora(int id = 1, DateOnly? vigenciaHasta = null) =>
        TipoDeIva.FromDb(
            id: id,
            codigo: "IVA_21",
            descripcion: "IVA 21%",
            porcentaje: 21m,
            aplicaIVA: true,
            activo: true,
            vigenciaDesde: new DateOnly(2024, 1, 1),
            vigenciaHasta: vigenciaHasta,
            predecesorId: null,
            fechaCreacion: DateTime.UtcNow,
            fechaModificacion: null);

    private static NuevaVersionTipoDeIvaCommand ValidCommand() => new(
        PredecesoraId: 1,
        NuevoPorcentaje: 27m,
        VigenciaDesde: new DateOnly(2025, 1, 1));

    public NuevaVersionTipoDeIvaCommandHandlerTests()
    {
        _handler = new NuevaVersionTipoDeIvaCommandHandler(_repo, _audit, TimeProvider.System);
        _repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(MakePredecesora());
        _repo.UpdateCierreVigenciaAsync(Arg.Any<int>(), Arg.Any<DateOnly>(), Arg.Any<CancellationToken>())
             .Returns(true);
        _repo.InsertAsync(Arg.Any<TipoDeIva>(), Arg.Any<CancellationToken>()).Returns(99);
    }

    // ── happy path ───────────────────────────────────────────────────────────

    [Fact]
    public async Task Handle_HappyPath_ReturnsDtoWithCorrectIds()
    {
        var result = await _handler.Handle(ValidCommand());

        Assert.Equal(1, result.PredecesoraId);
        Assert.Equal(99, result.NuevaVersionId);
    }

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

        await _repo.Received(1).UpdateCierreVigenciaAsync(
            1,
            new DateOnly(2024, 12, 31),  // vigenciaDesde(2025-01-01) - 1 day
            Arg.Any<CancellationToken>());
    }

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

        await _repo.Received(1).InsertAsync(
            Arg.Is<TipoDeIva>(e => e.Porcentaje == 27m && e.PredecesorId == 1),
            Arg.Any<CancellationToken>());
    }

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

        await _audit.Received(1).LogAsync(
            action: "tipo_iva.nueva_version",
            targetType: "TipoDeIva",
            targetId: Arg.Any<string>(),
            metadata: Arg.Any<object?>(),
            ct: Arg.Any<CancellationToken>());
    }

    // ── predecesora not found ────────────────────────────────────────────────

    [Fact]
    public async Task Handle_PredecesoraNotFound_ThrowsTipoDeIvaNotFoundException()
    {
        _repo.GetByIdAsync(999, Arg.Any<CancellationToken>()).Returns((TipoDeIva?)null);

        await Assert.ThrowsAsync<TipoDeIvaNotFoundException>(
            () => _handler.Handle(new NuevaVersionTipoDeIvaCommand(999, 27m, new DateOnly(2025, 1, 1))));
    }

    // ── predecesora ya cerrada ────────────────────────────────────────────────

    [Fact]
    public async Task Handle_PredecesoraYaCerrada_ThrowsPredecesorYaCerradoException()
    {
        _repo.GetByIdAsync(1, Arg.Any<CancellationToken>())
             .Returns(MakePredecesora(vigenciaHasta: new DateOnly(2024, 12, 31)));

        await Assert.ThrowsAsync<PredecesorYaCerradoException>(
            () => _handler.Handle(ValidCommand()));
    }

    // ── predecesora inactiva ──────────────────────────────────────────────────

    [Fact]
    public async Task Handle_PredecesoraInactiva_ThrowsPredecesorYaCerradoException()
    {
        var inactiva = TipoDeIva.FromDb(1, "IVA_21", "IVA 21%", 21m, true, false,
            new DateOnly(2024, 1, 1), null, null, DateTime.UtcNow, null);
        _repo.GetByIdAsync(1, Arg.Any<CancellationToken>()).Returns(inactiva);

        await Assert.ThrowsAsync<PredecesorYaCerradoException>(
            () => _handler.Handle(ValidCommand()));
    }

    // ── vigenciaDesde inválida ────────────────────────────────────────────────

    [Fact]
    public async Task Handle_VigenciaDesdeNotAfterPredecesora_ThrowsArgumentException()
    {
        var cmd = new NuevaVersionTipoDeIvaCommand(
            PredecesoraId: 1,
            NuevoPorcentaje: 27m,
            VigenciaDesde: new DateOnly(2024, 1, 1)); // same as predecesora

        await Assert.ThrowsAsync<ArgumentException>(
            () => _handler.Handle(cmd));
    }

    // ── race condition: UpdateCierreVigencia returns false ────────────────────

    [Fact]
    public async Task Handle_UpdateCierreVigenciaReturnsFalse_ThrowsPredecesorYaCerradoException()
    {
        _repo.UpdateCierreVigenciaAsync(Arg.Any<int>(), Arg.Any<DateOnly>(), Arg.Any<CancellationToken>())
             .Returns(false);

        await Assert.ThrowsAsync<PredecesorYaCerradoException>(
            () => _handler.Handle(ValidCommand()));
    }

    [Fact]
    public async Task Handle_UpdateCierreVigenciaReturnsFalse_InsertIsNeverCalled()
    {
        _repo.UpdateCierreVigenciaAsync(Arg.Any<int>(), Arg.Any<DateOnly>(), Arg.Any<CancellationToken>())
             .Returns(false);

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

        await _repo.DidNotReceive().InsertAsync(Arg.Any<TipoDeIva>(), Arg.Any<CancellationToken>());
    }

    // ── audit fail-closed ────────────────────────────────────────────────────

    [Fact]
    public async Task Handle_AuditLoggerThrows_ExceptionBubblesUp()
    {
        _audit.LogAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
                        Arg.Any<object?>(), Arg.Any<CancellationToken>())
              .Returns(Task.FromException(new InvalidOperationException("audit fail")));

        await Assert.ThrowsAsync<InvalidOperationException>(
            () => _handler.Handle(ValidCommand()));
    }

    [Fact]
    public async Task Handle_AuditLoggerThrows_InsertWasCalledButAuditFailed()
    {
        _audit.LogAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
                        Arg.Any<object?>(), Arg.Any<CancellationToken>())
              .Returns(Task.FromException(new InvalidOperationException("audit fail")));

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

        // Insert was called before audit — audit throwing means scope.Complete never runs
        await _repo.Received(1).InsertAsync(Arg.Any<TipoDeIva>(), Arg.Any<CancellationToken>());
        await _audit.Received(1).LogAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(),
                                          Arg.Any<object?>(), Arg.Any<CancellationToken>());
    }
}
