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.RefreshToken;
using Baya.Application.Models.Jwt;
using Baya.Domain.Entities.User;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using NSubstitute.ReturnsExtensions;

namespace Baya.Test.Foundation.Identity;

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

    private readonly IUnitOfWork _unitOfWork = Substitute.For<IUnitOfWork>();
    private readonly IUserSessionRepository _sessions = Substitute.For<IUserSessionRepository>();
    private readonly IJwtService _jwtService = Substitute.For<IJwtService>();
    private readonly IAppUserManager _userManager = Substitute.For<IAppUserManager>();
    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 RefreshTokenCommandHandler CreateHandler()
    {
        _clock.UtcNow.Returns(Now);
        _unitOfWork.UserSessionRepository.Returns(_sessions);
        _fieldEncryptor.Hash(Arg.Any<string>()).Returns("TOKEN_HASH");
        _platformConfig.GetConfig<int>(IdentityDefaults.SessionTtlDaysKey, Arg.Any<CancellationToken>()).Returns(30);
        _jwtService.GenerateAccessTokenAsync(Arg.Any<User>())
            .Returns(new JweAccessToken("new-access-token", Now.AddMinutes(15)));

        return new RefreshTokenCommandHandler(
            _unitOfWork, _jwtService, _userManager, _platformConfig, _fieldEncryptor, _clock, _currentUser,
            NullLogger<RefreshTokenCommandHandler>.Instance);
    }

    [Fact]
    public async Task Handle_ActiveSession_RotatesAndReturnsNewPair()
    {
        // Arrange
        var user = new User { Id = 7, IsActive = true };
        var session = new UserSession
        {
            UserId = 7,
            User = user,
            RefreshTokenHash = "TOKEN_HASH",
            ExpiresAt = Now.AddDays(10)
        };
        _sessions.GetByTokenHashAsync("TOKEN_HASH", Arg.Any<CancellationToken>()).Returns(session);
        _userManager.GetRoleAsync(user).Returns(["customer"]);
        var handler = CreateHandler();

        // Act
        var result = await handler.Handle(new RefreshTokenCommand("raw-refresh-token"), CancellationToken.None);

        // Assert
        Assert.True(result.IsSuccess);
        Assert.True(session.IsRevoked);
        Assert.Equal(Now, session.RevokedAt);
        Assert.Equal("new-access-token", result.Result.AccessToken);
        Assert.Contains("customer", result.Result.Roles);
        await _sessions.Received(1).AddAsync(Arg.Is<UserSession>(s => !s.IsRevoked && s.UserId == 7), Arg.Any<CancellationToken>());
        await _unitOfWork.Received(1).CommitAsync();
    }

    [Fact]
    public async Task Handle_RevokedSessionReplay_RevokesEverythingAndReturns401()
    {
        // Arrange — a token presented against an already-rotated session is a stolen-token signal.
        var session = new UserSession { UserId = 7, RefreshTokenHash = "TOKEN_HASH", IsRevoked = true, ExpiresAt = Now.AddDays(10) };
        _sessions.GetByTokenHashAsync("TOKEN_HASH", Arg.Any<CancellationToken>()).Returns(session);
        var handler = CreateHandler();

        // Act
        var result = await handler.Handle(new RefreshTokenCommand("raw-refresh-token"), CancellationToken.None);

        // Assert
        Assert.False(result.IsSuccess);
        Assert.True(result.IsUnauthorized);
        await _sessions.Received(1).RevokeAllActiveForUserAsync(7, Now, Arg.Any<CancellationToken>());
        await _sessions.DidNotReceive().AddAsync(Arg.Any<UserSession>(), Arg.Any<CancellationToken>());
    }

    [Fact]
    public async Task Handle_ExpiredSession_Returns401AndRevokesIt()
    {
        // Arrange
        var session = new UserSession { UserId = 7, RefreshTokenHash = "TOKEN_HASH", ExpiresAt = Now.AddMinutes(-1) };
        _sessions.GetByTokenHashAsync("TOKEN_HASH", Arg.Any<CancellationToken>()).Returns(session);
        var handler = CreateHandler();

        // Act
        var result = await handler.Handle(new RefreshTokenCommand("raw-refresh-token"), CancellationToken.None);

        // Assert
        Assert.True(result.IsUnauthorized);
        Assert.True(session.IsRevoked);
    }

    [Fact]
    public async Task Handle_UnknownToken_Returns401()
    {
        // Arrange
        _sessions.GetByTokenHashAsync(Arg.Any<string>(), Arg.Any<CancellationToken>()).ReturnsNull();
        var handler = CreateHandler();

        // Act
        var result = await handler.Handle(new RefreshTokenCommand("bogus"), CancellationToken.None);

        // Assert
        Assert.True(result.IsUnauthorized);
    }
}