using Baya.Application.Contracts;
using Baya.Application.Contracts.Common;
using Baya.Application.Contracts.Configuration;
using Baya.Application.Contracts.Identity;
using Baya.Application.Contracts.Persistence;
using Baya.Application.Features.Identity;
using Baya.Application.Features.Identity.Commands.VerifyOtp;
using Baya.Application.Models.Jwt;
using Baya.Domain.Entities.User;
using Microsoft.AspNetCore.Identity;
using NSubstitute;
using NSubstitute.ReturnsExtensions;

namespace Baya.Test.Foundation.Identity;

public class VerifyOtpCommandHandlerTests
{
    private static readonly DateTimeOffset Now = new(2026, 7, 1, 12, 0, 0, TimeSpan.Zero);

    private readonly IAppUserManager _userManager = Substitute.For<IAppUserManager>();
    private readonly IJwtService _jwtService = Substitute.For<IJwtService>();
    private readonly IUnitOfWork _unitOfWork = Substitute.For<IUnitOfWork>();
    private readonly IUserSessionRepository _sessions = Substitute.For<IUserSessionRepository>();
    private readonly IPlatformConfig _platformConfig = Substitute.For<IPlatformConfig>();
    private readonly IFieldEncryptor _fieldEncryptor = Substitute.For<IFieldEncryptor>();
    private readonly IDateTimeProvider _clock = Substitute.For<IDateTimeProvider>();
    private readonly ICurrentUser _currentUser = Substitute.For<ICurrentUser>();

    private VerifyOtpCommandHandler CreateHandler()
    {
        _clock.UtcNow.Returns(Now);
        _unitOfWork.UserSessionRepository.Returns(_sessions);
        _fieldEncryptor.Hash(Arg.Any<string>()).Returns("TOKEN_HASH");
        _platformConfig.GetConfig<int>(IdentityDefaults.OtpMaxAttemptsKey, Arg.Any<CancellationToken>()).Returns(5);
        _platformConfig.GetConfig<int>(IdentityDefaults.SessionTtlDaysKey, Arg.Any<CancellationToken>()).Returns(30);
        _jwtService.GenerateAccessTokenAsync(Arg.Any<User>())
            .Returns(new JweAccessToken("jwe-access-token", Now.AddMinutes(15)));

        return new VerifyOtpCommandHandler(
            _userManager, _jwtService, _unitOfWork, _platformConfig, _fieldEncryptor, _clock, _currentUser);
    }

    [Fact]
    public async Task Handle_ValidCodeForNewUser_ActivatesUserMintsTokensAndSession()
    {
        // Arrange
        var user = new User { PhoneNumber = "09123456789", PhoneNumberConfirmed = false };
        _userManager.GetUserByPhoneNumber("09123456789").Returns(user);
        _userManager.ChangePhoneNumber(user, "09123456789", "123456").Returns(IdentityResult.Success);
        _userManager.GetRoleAsync(user).Returns([]);
        var handler = CreateHandler();

        // Act
        var result = await handler.Handle(new VerifyOtpCommand("09123456789", "123456", "test-device"), CancellationToken.None);

        // Assert
        Assert.True(result.IsSuccess);
        Assert.True(result.Result.IsNewUser);
        Assert.Empty(result.Result.Roles);
        Assert.Equal("jwe-access-token", result.Result.AccessToken);
        Assert.False(string.IsNullOrEmpty(result.Result.RefreshToken));
        Assert.Equal(Now.AddDays(30), result.Result.RefreshExpiresAt);
        Assert.True(user.IsActive);
        Assert.Equal(Now, user.PhoneVerifiedAt);
        await _sessions.Received(1).AddAsync(
            Arg.Is<UserSession>(s => s.RefreshTokenHash == "TOKEN_HASH" && !s.IsRevoked && s.DeviceInfo == "test-device"),
            Arg.Any<CancellationToken>());
        await _unitOfWork.Received(1).CommitAsync();
    }

    [Fact]
    public async Task Handle_WrongCode_IncrementsAttemptsAndFailsSafely()
    {
        // Arrange
        var user = new User { PhoneNumber = "09123456789", PhoneNumberConfirmed = true };
        _userManager.GetUserByPhoneNumber("09123456789").Returns(user);
        _userManager.VerifyUserCode(user, "999999")
            .Returns(IdentityResult.Failed(new IdentityError { Description = "Incorrect Code" }));
        var handler = CreateHandler();

        // Act
        var result = await handler.Handle(new VerifyOtpCommand("09123456789", "999999"), CancellationToken.None);

        // Assert
        Assert.False(result.IsSuccess);
        await _userManager.Received(1).IncrementAccessFailedCountAsync(user);
        await _sessions.DidNotReceive().AddAsync(Arg.Any<UserSession>(), Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task Handle_TooManyFailedAttempts_RefusesWithoutCheckingTheCode()
    {
        // Arrange
        var user = new User { PhoneNumber = "09123456789", PhoneNumberConfirmed = true, AccessFailedCount = 5 };
        _userManager.GetUserByPhoneNumber("09123456789").Returns(user);
        var handler = CreateHandler();

        // Act
        var result = await handler.Handle(new VerifyOtpCommand("09123456789", "123456"), CancellationToken.None);

        // Assert
        Assert.False(result.IsSuccess);
        await _userManager.DidNotReceive().VerifyUserCode(Arg.Any<User>(), Arg.Any<string>());
    }

    [Fact]
    public async Task Handle_UnknownPhone_FailsWithTheSameSafeMessageAsAWrongCode()
    {
        // Arrange — no enumeration: unknown phone and wrong code are indistinguishable.
        _userManager.GetUserByPhoneNumber(Arg.Any<string>()).ReturnsNull();
        var handler = CreateHandler();

        // Act
        var result = await handler.Handle(new VerifyOtpCommand("09123456789", "123456"), CancellationToken.None);

        // Assert
        Assert.False(result.IsSuccess);
        Assert.Contains(result.ErrorMessages, e => e.Value.Contains("invalid or expired", StringComparison.OrdinalIgnoreCase));
    }
}