using NSubstitute;
using SIGCM2.Application.Abstractions.Persistence;
using SIGCM2.Application.Abstractions.Security;
using SIGCM2.Application.Audit;
using SIGCM2.Application.Usuarios.Create;
using SIGCM2.Domain.Entities;
using SIGCM2.Domain.Exceptions;

namespace SIGCM2.Application.Tests.Usuarios.Create;

public class CreateUsuarioCommandHandlerTests
{
    private readonly IUsuarioRepository _repository = Substitute.For<IUsuarioRepository>();
    private readonly IPasswordHasher _hasher = Substitute.For<IPasswordHasher>();
    private readonly IAuditLogger _audit = Substitute.For<IAuditLogger>();
    private readonly CreateUsuarioCommandHandler _handler;

    private static CreateUsuarioCommand ValidCommand() => new(
        Username: "operador1",
        Password: "Secreto123",
        Nombre: "Juan",
        Apellido: "Pérez",
        Email: null,
        Rol: "vendedor");

    public CreateUsuarioCommandHandlerTests()
    {
        _handler = new CreateUsuarioCommandHandler(_repository, _hasher, _audit);
    }

    // ── exists → throws ──────────────────────────────────────────────────────

    [Fact]
    public async Task Handle_UsernameAlreadyExists_ThrowsUsernameAlreadyExistsException()
    {
        _repository.ExistsByUsernameAsync("operador1", Arg.Any<CancellationToken>())
            .Returns(true);

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

    [Fact]
    public async Task Handle_UsernameAlreadyExists_DoesNotCallAddAsync()
    {
        _repository.ExistsByUsernameAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(true);

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

        await _repository.DidNotReceive().AddAsync(Arg.Any<Usuario>(), Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task Handle_UsernameAlreadyExists_ExceptionContainsUsername()
    {
        _repository.ExistsByUsernameAsync("operador1", Arg.Any<CancellationToken>())
            .Returns(true);

        var ex = await Assert.ThrowsAsync<UsernameAlreadyExistsException>(
            () => _handler.Handle(ValidCommand()));

        Assert.Equal("operador1", ex.Username);
    }

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

    [Fact]
    public async Task Handle_HappyPath_HashesPasswordBeforePersisting()
    {
        _repository.ExistsByUsernameAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(false);
        _hasher.Hash("Secreto123").Returns("$2a$12$hashed");
        _repository.AddAsync(Arg.Any<Usuario>(), Arg.Any<CancellationToken>()).Returns(42);

        await _handler.Handle(ValidCommand());

        // AddAsync must be called with the hashed value, not the plain password
        await _repository.Received(1).AddAsync(
            Arg.Is<Usuario>(u => u.PasswordHash == "$2a$12$hashed"),
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task Handle_HappyPath_NeverPersistsPlainPassword()
    {
        _repository.ExistsByUsernameAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(false);
        _hasher.Hash(Arg.Any<string>()).Returns("$2a$12$hashed");
        _repository.AddAsync(Arg.Any<Usuario>(), Arg.Any<CancellationToken>()).Returns(1);

        await _handler.Handle(ValidCommand());

        await _repository.Received(1).AddAsync(
            Arg.Is<Usuario>(u => u.PasswordHash != "Secreto123"),
            Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task Handle_HappyPath_CallsAddAsyncOnce()
    {
        _repository.ExistsByUsernameAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(false);
        _hasher.Hash(Arg.Any<string>()).Returns("$2a$12$hashed");
        _repository.AddAsync(Arg.Any<Usuario>(), Arg.Any<CancellationToken>()).Returns(7);

        await _handler.Handle(ValidCommand());

        await _repository.Received(1).AddAsync(Arg.Any<Usuario>(), Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task Handle_HappyPath_ReturnsDtoWithIdFromRepository()
    {
        _repository.ExistsByUsernameAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(false);
        _hasher.Hash(Arg.Any<string>()).Returns("$2a$12$hashed");
        _repository.AddAsync(Arg.Any<Usuario>(), Arg.Any<CancellationToken>()).Returns(42);

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

        Assert.Equal(42, result.Id);
    }

    [Fact]
    public async Task Handle_HappyPath_DtoContainsCorrectFields()
    {
        _repository.ExistsByUsernameAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(false);
        _hasher.Hash(Arg.Any<string>()).Returns("$2a$12$hashed");
        _repository.AddAsync(Arg.Any<Usuario>(), Arg.Any<CancellationToken>()).Returns(10);

        var cmd = new CreateUsuarioCommand("user1", "Pass1234", "Ana", "García", "ana@example.com", "admin");
        var result = await _handler.Handle(cmd);

        Assert.Equal("user1", result.Username);
        Assert.Equal("Ana", result.Nombre);
        Assert.Equal("García", result.Apellido);
        Assert.Equal("ana@example.com", result.Email);
        Assert.Equal("admin", result.Rol);
        Assert.True(result.Activo);
    }

    [Fact]
    public async Task Handle_HappyPath_DtoDoesNotContainPasswordHash()
    {
        // UsuarioCreatedDto must not expose PasswordHash — compile-time check via reflection
        _repository.ExistsByUsernameAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(false);
        _hasher.Hash(Arg.Any<string>()).Returns("$2a$12$secret");
        _repository.AddAsync(Arg.Any<Usuario>(), Arg.Any<CancellationToken>()).Returns(1);

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

        var props = result.GetType().GetProperties().Select(p => p.Name);
        Assert.DoesNotContain("PasswordHash", props);
    }

    [Fact]
    public async Task Handle_HappyPath_NewUserIsActive()
    {
        _repository.ExistsByUsernameAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
            .Returns(false);
        _hasher.Hash(Arg.Any<string>()).Returns("$2a$12$hashed");
        _repository.AddAsync(
            Arg.Is<Usuario>(u => u.Activo && u.PermisosJson == "[]"),
            Arg.Any<CancellationToken>()).Returns(5);

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

        Assert.True(result.Activo);
    }
}
