using FluentAssertions;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Time.Testing;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using SIGCM2.Application.Abstractions.Persistence;
using SIGCM2.Application.Audit;
using SIGCM2.Application.Rubros;
using SIGCM2.Application.Rubros.Create;
using SIGCM2.Domain.Entities;
using SIGCM2.Domain.Exceptions;

namespace SIGCM2.Application.Tests.Rubros.Create;

public class CreateRubroCommandHandlerTests
{
    private readonly IRubroRepository _repo = Substitute.For<IRubroRepository>();
    private readonly IAuditLogger _audit = Substitute.For<IAuditLogger>();
    private readonly FakeTimeProvider _timeProvider = new(new DateTimeOffset(2026, 4, 18, 12, 0, 0, TimeSpan.Zero));
    private readonly IOptions<RubrosOptions> _options = Options.Create(new RubrosOptions { MaxDepth = 10 });
    private readonly IAvisoQueryRepository _avisoQuery = Substitute.For<IAvisoQueryRepository>();
    private readonly CreateRubroCommandHandler _handler;

    public CreateRubroCommandHandlerTests()
    {
        _repo.ExistsByNombreUnderParentAsync(Arg.Any<int?>(), Arg.Any<string>(), Arg.Any<int?>(), Arg.Any<CancellationToken>())
             .Returns(false);
        _repo.GetMaxOrdenAsync(Arg.Any<int?>(), Arg.Any<CancellationToken>())
             .Returns(0);
        _repo.AddAsync(Arg.Any<Rubro>(), Arg.Any<CancellationToken>())
             .Returns(1);
        _repo.GetDepthAsync(Arg.Any<int?>(), Arg.Any<CancellationToken>())
             .Returns(0);
        // Default: no avisos (stub behavior)
        _avisoQuery.CountAvisosEnRubroAsync(Arg.Any<int>(), Arg.Any<CancellationToken>())
                   .Returns(0);

        _handler = new CreateRubroCommandHandler(_repo, _audit, _timeProvider, _options, _avisoQuery);
    }

    private static CreateRubroCommand RootCommand() => new("Autos", ParentId: null, TarifarioBaseId: null);
    private static CreateRubroCommand ChildCommand(int parentId) => new("Sedanes", ParentId: parentId, TarifarioBaseId: null);

    // ── Happy path: root ─────────────────────────────────────────────────────

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

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

        result.Id.Should().Be(42);
    }

    [Fact]
    public async Task Handle_HappyPath_Root_CallsAddAsync()
    {
        await _handler.Handle(RootCommand());

        await _repo.Received(1).AddAsync(
            Arg.Is<Rubro>(r => r.Nombre == "Autos" && r.ParentId == null),
            Arg.Any<CancellationToken>());
    }

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

        await _handler.Handle(RootCommand());

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

    // ── Happy path: child — uses GetMaxOrdenAsync ────────────────────────────

    [Fact]
    public async Task Handle_HappyPath_Child_UsesMaxOrdenForOrden()
    {
        // GetMaxOrdenAsync returns the next available slot (MAX+1 semantics in the repo)
        _repo.GetMaxOrdenAsync((int?)5, Arg.Any<CancellationToken>()).Returns(3);
        var parent = new Rubro(5, null, "ParentRubro", 0, activo: true, tarifarioBaseId: null,
            fechaCreacion: DateTime.UtcNow, fechaModificacion: null);
        _repo.GetByIdAsync(5, Arg.Any<CancellationToken>()).Returns(parent);

        await _handler.Handle(ChildCommand(parentId: 5));

        await _repo.Received(1).AddAsync(
            Arg.Is<Rubro>(r => r.Orden == 3),
            Arg.Any<CancellationToken>());
    }

    // ── Parent not found → RubroNotFoundException ────────────────────────────

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

        var act = () => _handler.Handle(ChildCommand(parentId: 999));

        await act.Should().ThrowAsync<RubroNotFoundException>()
            .Where(ex => ex.Id == 999);
    }

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

        try { await _handler.Handle(ChildCommand(parentId: 999)); } catch { }

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

    // ── Parent inactive → RubroPadreInactivoException ───────────────────────

    [Fact]
    public async Task Handle_ParentInactive_ThrowsRubroPadreInactivoException()
    {
        var inactiveParent = new Rubro(7, null, "InactivoParent", 0, activo: false, tarifarioBaseId: null,
            fechaCreacion: DateTime.UtcNow, fechaModificacion: null);
        _repo.GetByIdAsync(7, Arg.Any<CancellationToken>()).Returns(inactiveParent);

        var act = () => _handler.Handle(ChildCommand(parentId: 7));

        await act.Should().ThrowAsync<RubroPadreInactivoException>()
            .Where(ex => ex.ParentId == 7);
    }

    // ── Duplicate name (CI) → RubroNombreDuplicadoEnPadreException ──────────

    [Fact]
    public async Task Handle_DuplicateName_ThrowsRubroNombreDuplicadoEnPadreException()
    {
        _repo.ExistsByNombreUnderParentAsync(null, "Autos", null, Arg.Any<CancellationToken>())
             .Returns(true);

        var act = () => _handler.Handle(RootCommand());

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

    // ── Depth exceeded → RubroMaxDepthExceededException ─────────────────────

    [Fact]
    public async Task Handle_DepthExceeded_ThrowsRubroMaxDepthExceededException()
    {
        // MaxDepth=10, parent is at depth 10 → creating child would be depth 11
        _repo.GetDepthAsync(5, Arg.Any<CancellationToken>()).Returns(10);
        var parent = new Rubro(5, null, "DeepParent", 0, activo: true, tarifarioBaseId: null,
            fechaCreacion: DateTime.UtcNow, fechaModificacion: null);
        _repo.GetByIdAsync(5, Arg.Any<CancellationToken>()).Returns(parent);

        var act = () => _handler.Handle(ChildCommand(parentId: 5));

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

    [Fact]
    public async Task Handle_DepthAtMaxAllowed_Succeeds()
    {
        // MaxDepth=10, parent at depth 9 → child at depth 10 is allowed
        _repo.GetDepthAsync(5, Arg.Any<CancellationToken>()).Returns(9);
        var parent = new Rubro(5, null, "DeepParent", 0, activo: true, tarifarioBaseId: null,
            fechaCreacion: DateTime.UtcNow, fechaModificacion: null);
        _repo.GetByIdAsync(5, Arg.Any<CancellationToken>()).Returns(parent);
        _repo.AddAsync(Arg.Any<Rubro>(), Arg.Any<CancellationToken>()).Returns(99);

        var result = await _handler.Handle(ChildCommand(parentId: 5));

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

    // ── CAT-002: Guard padre sin avisos ──────────────────────────────────────

    [Fact]
    public async Task Handle_ParentTieneAvisos_Throws_RubroPadreEsHojaConAvisosException()
    {
        const int parentId = 5;
        var parent = new Rubro(parentId, null, "ParentConAvisos", 0, activo: true, tarifarioBaseId: null,
            fechaCreacion: DateTime.UtcNow, fechaModificacion: null);
        _repo.GetByIdAsync(parentId, Arg.Any<CancellationToken>()).Returns(parent);
        _avisoQuery.CountAvisosEnRubroAsync(parentId, Arg.Any<CancellationToken>()).Returns(3);

        var act = () => _handler.Handle(ChildCommand(parentId: parentId));

        await act.Should().ThrowAsync<RubroPadreEsHojaConAvisosException>()
            .Where(ex => ex.ParentId == parentId && ex.CantidadAvisos == 3);
    }

    [Fact]
    public async Task Handle_ParentTieneCeroAvisos_DoesNotThrow()
    {
        const int parentId = 5;
        var parent = new Rubro(parentId, null, "ParentSinAvisos", 0, activo: true, tarifarioBaseId: null,
            fechaCreacion: DateTime.UtcNow, fechaModificacion: null);
        _repo.GetByIdAsync(parentId, Arg.Any<CancellationToken>()).Returns(parent);
        _avisoQuery.CountAvisosEnRubroAsync(parentId, Arg.Any<CancellationToken>()).Returns(0);
        _repo.AddAsync(Arg.Any<Rubro>(), Arg.Any<CancellationToken>()).Returns(10);

        var result = await _handler.Handle(ChildCommand(parentId: parentId));

        result.Id.Should().Be(10);
    }

    [Fact]
    public async Task Handle_ParentNull_SkipsAvisosGuard()
    {
        // Root creation — no parent → CountAvisosEnRubroAsync should NOT be called
        await _handler.Handle(RootCommand());

        await _avisoQuery.DidNotReceive().CountAvisosEnRubroAsync(Arg.Any<int>(), Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task Handle_GuardOrder_ParentInactivo_Wins_OverAvisos()
    {
        // Inactive parent with avisos → RubroPadreInactivoException (not avisos exception)
        const int parentId = 7;
        var inactiveParent = new Rubro(parentId, null, "InactivoConAvisos", 0, activo: false, tarifarioBaseId: null,
            fechaCreacion: DateTime.UtcNow, fechaModificacion: null);
        _repo.GetByIdAsync(parentId, Arg.Any<CancellationToken>()).Returns(inactiveParent);
        _avisoQuery.CountAvisosEnRubroAsync(parentId, Arg.Any<CancellationToken>()).Returns(3);

        var act = () => _handler.Handle(ChildCommand(parentId: parentId));

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

    [Fact]
    public async Task Handle_GuardOrder_Avisos_Wins_OverDepth()
    {
        // Parent at MAX_DEPTH AND has avisos → RubroPadreEsHojaConAvisosException (avisos guard fires first)
        const int parentId = 5;
        var parent = new Rubro(parentId, null, "ParentAtMaxDepth", 0, activo: true, tarifarioBaseId: null,
            fechaCreacion: DateTime.UtcNow, fechaModificacion: null);
        _repo.GetByIdAsync(parentId, Arg.Any<CancellationToken>()).Returns(parent);
        _avisoQuery.CountAvisosEnRubroAsync(parentId, Arg.Any<CancellationToken>()).Returns(2);
        _repo.GetDepthAsync(parentId, Arg.Any<CancellationToken>()).Returns(10); // at MaxDepth

        var act = () => _handler.Handle(ChildCommand(parentId: parentId));

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