using FluentAssertions;
using Microsoft.Extensions.Time.Testing;
using NSubstitute;
using SIGCM2.Application.Abstractions.Persistence;
using SIGCM2.Application.Avisos;
using SIGCM2.Application.Rubros.GetById;
using SIGCM2.Application.Rubros.GetTree;
using SIGCM2.Domain.Entities;
using SIGCM2.Domain.Exceptions;

namespace SIGCM2.Application.Tests.Rubros.GetTree;

public class GetRubroTreeQueryHandlerTests
{
    private static readonly FakeTimeProvider FakeTime = new(new DateTimeOffset(2026, 4, 18, 12, 0, 0, TimeSpan.Zero));
    private readonly IRubroRepository _repo = Substitute.For<IRubroRepository>();
    private readonly IAvisoQueryRepository _avisoQuery = Substitute.For<IAvisoQueryRepository>();

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

    // ── GetRubroTreeQueryHandler ─────────────────────────────────────────────

    [Fact]
    public async Task GetTree_OnlyActivos_ByDefault_ReturnsActiveTree()
    {
        _repo.GetAllAsync(false, Arg.Any<CancellationToken>())
             .Returns(new[] { MakeRubro(1), MakeRubro(2) });
        _avisoQuery.CountAvisosBatchAsync(Arg.Any<IReadOnlyCollection<int>>(), Arg.Any<CancellationToken>())
                   .Returns(new Dictionary<int, int>());

        var handler = new GetRubroTreeQueryHandler(_repo, _avisoQuery);
        var result = await handler.Handle(new GetRubroTreeQuery(IncluirInactivos: false));

        result.Should().HaveCount(2);
    }

    [Fact]
    public async Task GetTree_IncluirInactivos_CallsRepoWithTrue()
    {
        _repo.GetAllAsync(true, Arg.Any<CancellationToken>())
             .Returns(new[] { MakeRubro(1), MakeRubro(2, activo: false) });
        _avisoQuery.CountAvisosBatchAsync(Arg.Any<IReadOnlyCollection<int>>(), Arg.Any<CancellationToken>())
                   .Returns(new Dictionary<int, int>());

        var handler = new GetRubroTreeQueryHandler(_repo, _avisoQuery);
        var result = await handler.Handle(new GetRubroTreeQuery(IncluirInactivos: true));

        await _repo.Received(1).GetAllAsync(true, Arg.Any<CancellationToken>());
        result.Should().HaveCount(2); // both roots
    }

    [Fact]
    public async Task GetTree_Empty_ReturnsEmptyList()
    {
        _repo.GetAllAsync(Arg.Any<bool>(), Arg.Any<CancellationToken>())
             .Returns(Array.Empty<Rubro>());
        _avisoQuery.CountAvisosBatchAsync(Arg.Any<IReadOnlyCollection<int>>(), Arg.Any<CancellationToken>())
                   .Returns(new Dictionary<int, int>());

        var handler = new GetRubroTreeQueryHandler(_repo, _avisoQuery);
        var result = await handler.Handle(new GetRubroTreeQuery(IncluirInactivos: false));

        result.Should().BeEmpty();
    }

    // ── CAT-002: TieneAvisos populated via batch ─────────────────────────────

    [Fact]
    public async Task Handle_PopulatesTieneAvisos_True_WhenBatchResultContainsCount()
    {
        _repo.GetAllAsync(false, Arg.Any<CancellationToken>())
             .Returns(new[] { MakeRubro(1), MakeRubro(2), MakeRubro(3) });
        _avisoQuery.CountAvisosBatchAsync(Arg.Any<IReadOnlyCollection<int>>(), Arg.Any<CancellationToken>())
                   .Returns(new Dictionary<int, int> { { 1, 2 } });

        var handler = new GetRubroTreeQueryHandler(_repo, _avisoQuery);
        var result = await handler.Handle(new GetRubroTreeQuery(IncluirInactivos: false));

        result.Single(n => n.Id == 1).TieneAvisos.Should().BeTrue();
        result.Single(n => n.Id == 2).TieneAvisos.Should().BeFalse();
        result.Single(n => n.Id == 3).TieneAvisos.Should().BeFalse();
    }

    [Fact]
    public async Task Handle_CallsBatchExactlyOnce_WithAllRubroIds()
    {
        _repo.GetAllAsync(false, Arg.Any<CancellationToken>())
             .Returns(new[] { MakeRubro(1), MakeRubro(2), MakeRubro(3) });
        _avisoQuery.CountAvisosBatchAsync(Arg.Any<IReadOnlyCollection<int>>(), Arg.Any<CancellationToken>())
                   .Returns(new Dictionary<int, int>());

        var handler = new GetRubroTreeQueryHandler(_repo, _avisoQuery);
        await handler.Handle(new GetRubroTreeQuery(IncluirInactivos: false));

        await _avisoQuery.Received(1).CountAvisosBatchAsync(
            Arg.Any<IReadOnlyCollection<int>>(),
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task Handle_EmptyTree_CallsBatchWithEmptyList()
    {
        _repo.GetAllAsync(Arg.Any<bool>(), Arg.Any<CancellationToken>())
             .Returns(Array.Empty<Rubro>());
        _avisoQuery.CountAvisosBatchAsync(Arg.Any<IReadOnlyCollection<int>>(), Arg.Any<CancellationToken>())
                   .Returns(new Dictionary<int, int>());

        var handler = new GetRubroTreeQueryHandler(_repo, _avisoQuery);
        await handler.Handle(new GetRubroTreeQuery(IncluirInactivos: false));

        await _avisoQuery.Received(1).CountAvisosBatchAsync(
            Arg.Is<IReadOnlyCollection<int>>(ids => ids.Count == 0),
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task Handle_StubBehavior_AllNodesTieneAvisosFalse()
    {
        _repo.GetAllAsync(false, Arg.Any<CancellationToken>())
             .Returns(new[] { MakeRubro(1), MakeRubro(2) });

        var handler = new GetRubroTreeQueryHandler(_repo, new NullAvisoQueryRepository());
        var result = await handler.Handle(new GetRubroTreeQuery(IncluirInactivos: false));

        result.Should().AllSatisfy(n => n.TieneAvisos.Should().BeFalse());
    }

    // ── GetRubroByIdQueryHandler ─────────────────────────────────────────────

    [Fact]
    public async Task GetById_Found_Active_ReturnsDto()
    {
        _repo.GetByIdAsync(5, Arg.Any<CancellationToken>()).Returns(MakeRubro(5));

        var handler = new GetRubroByIdQueryHandler(_repo);
        var result = await handler.Handle(new GetRubroByIdQuery(Id: 5));

        result.Should().NotBeNull();
        result!.Id.Should().Be(5);
    }

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

        var handler = new GetRubroByIdQueryHandler(_repo);
        var act = () => handler.Handle(new GetRubroByIdQuery(Id: 99));

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