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

namespace SIGCM2.Application.Tests.Rubros.Move;

public class MoveRubroCommandHandlerTests
{
    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 MoveRubroCommandHandler _handler;

    private static Rubro MakeRubro(int id, int? parentId = null, bool activo = true)
        => new(id, parentId, $"Rubro{id}", 0, activo, null, DateTime.UtcNow, null);

    public MoveRubroCommandHandlerTests()
    {
        _repo.GetDescendantsAsync(Arg.Any<int>(), Arg.Any<CancellationToken>())
             .Returns(Array.Empty<Rubro>());
        _repo.ExistsByNombreUnderParentAsync(Arg.Any<int?>(), Arg.Any<string>(), Arg.Any<int?>(), Arg.Any<CancellationToken>())
             .Returns(false);
        _repo.GetDepthAsync(Arg.Any<int?>(), Arg.Any<CancellationToken>())
             .Returns(0);
        _repo.GetMaxOrdenAsync(Arg.Any<int?>(), Arg.Any<CancellationToken>())
             .Returns(0);
        // Default: no avisos
        _avisoQuery.CountAvisosEnRubroAsync(Arg.Any<int>(), Arg.Any<CancellationToken>())
                   .Returns(0);

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

    // ── Happy path: move to other parent ────────────────────────────────────

    [Fact]
    public async Task Handle_HappyPath_Move_ReturnsMovedDto()
    {
        var rubro = MakeRubro(8, parentId: 2);
        var newParent = MakeRubro(20, parentId: 1);
        _repo.GetByIdAsync(8, Arg.Any<CancellationToken>()).Returns(rubro);
        _repo.GetByIdAsync(20, Arg.Any<CancellationToken>()).Returns(newParent);

        var result = await _handler.Handle(new MoveRubroCommand(Id: 8, NuevoParentId: 20, NuevoOrden: 0));

        result.Id.Should().Be(8);
        result.ParentId.Should().Be(20);
    }

    [Fact]
    public async Task Handle_HappyPath_Move_CallsAuditLogWithParentTransition()
    {
        var rubro = MakeRubro(8, parentId: 2);
        var newParent = MakeRubro(20, parentId: 1);
        _repo.GetByIdAsync(8, Arg.Any<CancellationToken>()).Returns(rubro);
        _repo.GetByIdAsync(20, Arg.Any<CancellationToken>()).Returns(newParent);

        await _handler.Handle(new MoveRubroCommand(Id: 8, NuevoParentId: 20, NuevoOrden: 0));

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

    // ── Move to root (nuevoParentId null) ─────────────────────────────────

    [Fact]
    public async Task Handle_MoveToRoot_SetsParentIdNull()
    {
        var rubro = MakeRubro(8, parentId: 3);
        _repo.GetByIdAsync(8, Arg.Any<CancellationToken>()).Returns(rubro);

        var result = await _handler.Handle(new MoveRubroCommand(Id: 8, NuevoParentId: null, NuevoOrden: 5));

        result.ParentId.Should().BeNull();
        result.Orden.Should().Be(5);
    }

    // ── Rubro not found → RubroNotFoundException ─────────────────────────

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

        var act = () => _handler.Handle(new MoveRubroCommand(Id: 99, NuevoParentId: 1, NuevoOrden: 0));

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

    // ── Cycle detection ───────────────────────────────────────────────────

    [Fact]
    public async Task Handle_DirectChildAsNewParent_ThrowsRubroCycleDetectedException()
    {
        var rubro = MakeRubro(5, parentId: null);
        _repo.GetByIdAsync(5, Arg.Any<CancellationToken>()).Returns(rubro);
        // Descendant id=10 would be the new parent
        _repo.GetDescendantsAsync(5, Arg.Any<CancellationToken>())
             .Returns(new[] { MakeRubro(10, parentId: 5) });

        var act = () => _handler.Handle(new MoveRubroCommand(Id: 5, NuevoParentId: 10, NuevoOrden: 0));

        await act.Should().ThrowAsync<RubroCycleDetectedException>()
            .Where(ex => ex.RubroId == 5 && ex.NuevoParentId == 10);
    }

    [Fact]
    public async Task Handle_DeepDescendantAsNewParent_ThrowsRubroCycleDetectedException()
    {
        var rubro = MakeRubro(5, parentId: null);
        _repo.GetByIdAsync(5, Arg.Any<CancellationToken>()).Returns(rubro);
        _repo.GetDescendantsAsync(5, Arg.Any<CancellationToken>())
             .Returns(new[] { MakeRubro(10, 5), MakeRubro(15, 10) });

        var act = () => _handler.Handle(new MoveRubroCommand(Id: 5, NuevoParentId: 15, NuevoOrden: 0));

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

    // ── New parent inactive → RubroPadreInactivoException ─────────────────

    [Fact]
    public async Task Handle_NewParentInactive_ThrowsRubroPadreInactivoException()
    {
        var rubro = MakeRubro(8, parentId: 2);
        var inactiveParent = MakeRubro(20, activo: false);
        _repo.GetByIdAsync(8, Arg.Any<CancellationToken>()).Returns(rubro);
        _repo.GetByIdAsync(20, Arg.Any<CancellationToken>()).Returns(inactiveParent);

        var act = () => _handler.Handle(new MoveRubroCommand(Id: 8, NuevoParentId: 20, NuevoOrden: 0));

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

    // ── Duplicate name under new parent ────────────────────────────────────

    [Fact]
    public async Task Handle_DuplicateNameUnderNewParent_ThrowsRubroNombreDuplicadoEnPadreException()
    {
        var rubro = MakeRubro(8, parentId: 2);
        var newParent = MakeRubro(20);
        _repo.GetByIdAsync(8, Arg.Any<CancellationToken>()).Returns(rubro);
        _repo.GetByIdAsync(20, Arg.Any<CancellationToken>()).Returns(newParent);
        _repo.ExistsByNombreUnderParentAsync((int?)20, rubro.Nombre, 8, Arg.Any<CancellationToken>())
             .Returns(true);

        var act = () => _handler.Handle(new MoveRubroCommand(Id: 8, NuevoParentId: 20, NuevoOrden: 0));

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

    // ── Depth exceeded ─────────────────────────────────────────────────────

    [Fact]
    public async Task Handle_DepthExceeded_ThrowsRubroMaxDepthExceededException()
    {
        var rubro = MakeRubro(8, parentId: 2);
        var newParent = MakeRubro(20);
        _repo.GetByIdAsync(8, Arg.Any<CancellationToken>()).Returns(rubro);
        _repo.GetByIdAsync(20, Arg.Any<CancellationToken>()).Returns(newParent);
        _repo.GetDepthAsync((int?)20, Arg.Any<CancellationToken>()).Returns(10); // at MaxDepth

        var act = () => _handler.Handle(new MoveRubroCommand(Id: 8, NuevoParentId: 20, NuevoOrden: 0));

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

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

    [Fact]
    public async Task Handle_NuevoParentTieneAvisos_Throws_RubroPadreEsHojaConAvisosException()
    {
        const int nuevoParentId = 20;
        var rubro = MakeRubro(8, parentId: 2);
        var newParent = MakeRubro(nuevoParentId);
        _repo.GetByIdAsync(8, Arg.Any<CancellationToken>()).Returns(rubro);
        _repo.GetByIdAsync(nuevoParentId, Arg.Any<CancellationToken>()).Returns(newParent);
        _avisoQuery.CountAvisosEnRubroAsync(nuevoParentId, Arg.Any<CancellationToken>()).Returns(2);

        var act = () => _handler.Handle(new MoveRubroCommand(Id: 8, NuevoParentId: nuevoParentId, NuevoOrden: 0));

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

    [Fact]
    public async Task Handle_NuevoParentTieneCeroAvisos_DoesNotThrow()
    {
        const int nuevoParentId = 20;
        var rubro = MakeRubro(8, parentId: 2);
        var newParent = MakeRubro(nuevoParentId);
        _repo.GetByIdAsync(8, Arg.Any<CancellationToken>()).Returns(rubro);
        _repo.GetByIdAsync(nuevoParentId, Arg.Any<CancellationToken>()).Returns(newParent);
        _avisoQuery.CountAvisosEnRubroAsync(nuevoParentId, Arg.Any<CancellationToken>()).Returns(0);

        var result = await _handler.Handle(new MoveRubroCommand(Id: 8, NuevoParentId: nuevoParentId, NuevoOrden: 0));

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

    [Fact]
    public async Task Handle_NuevoParentEsNull_SkipsAvisosGuard()
    {
        // Move to root — no parent to check avisos for
        var rubro = MakeRubro(8, parentId: 2);
        _repo.GetByIdAsync(8, Arg.Any<CancellationToken>()).Returns(rubro);

        await _handler.Handle(new MoveRubroCommand(Id: 8, NuevoParentId: null, NuevoOrden: 0));

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

    [Fact]
    public async Task Handle_CycleCheck_Wins_OverAvisosGuard()
    {
        // Cycle check fires before avisos guard
        const int nuevoParentId = 10;
        var rubro = MakeRubro(5, parentId: null);
        _repo.GetByIdAsync(5, Arg.Any<CancellationToken>()).Returns(rubro);
        // nuevoParentId IS a descendant (cycle)
        _repo.GetDescendantsAsync(5, Arg.Any<CancellationToken>())
             .Returns(new[] { MakeRubro(nuevoParentId, parentId: 5) });
        _avisoQuery.CountAvisosEnRubroAsync(nuevoParentId, Arg.Any<CancellationToken>()).Returns(3);

        var act = () => _handler.Handle(new MoveRubroCommand(Id: 5, NuevoParentId: nuevoParentId, NuevoOrden: 0));

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