using OmsorgCore.Application.Services;
using OmsorgCore.Domain.Entities;
using OmsorgCore.Tests.TestDoubles;

namespace OmsorgCore.Tests.Services;

public class PasswordResetServiceTests
{
    private static User CreateUserWithEmployee(string username = "pflegekraft", string email = "pflegekraft@example.com") => new()
    {
        Username = username,
        PasswordHash = "hashed:secret",
        IsActive = true,
        RoleId = Guid.NewGuid(),
        Role = new Role { Name = "Aussendienst" },
        EmployeeId = Guid.NewGuid(),
        Employee = new Employee { Email = email }
    };

    private static PasswordResetService CreateSut(
        FakeUserRepository users,
        FakePasswordResetCodeRepository? codes = null,
        FakePasswordResetCodeGenerator? generator = null,
        FakeRefreshTokenRepository? refreshTokens = null,
        FakePasswordPolicy? passwordPolicy = null)
    {
        return new PasswordResetService(
            users,
            codes ?? new FakePasswordResetCodeRepository(),
            generator ?? new FakePasswordResetCodeGenerator(),
            new FakePasswordHasher(),
            refreshTokens ?? new FakeRefreshTokenRepository(),
            passwordPolicy ?? new FakePasswordPolicy());
    }

    [Fact]
    public async Task RequestResetAsync_UnknownUsername_ReturnsCannotReset()
    {
        var sut = CreateSut(new FakeUserRepository());

        var result = await sut.RequestResetAsync("does-not-exist");

        Assert.Equal(PasswordResetRequestStatus.CannotReset, result.Status);
    }

    [Fact]
    public async Task RequestResetAsync_UserWithoutEmployeeLink_ReturnsCannotReset()
    {
        var user = new User
        {
            Username = "admin",
            PasswordHash = "hashed:secret",
            IsActive = true,
            RoleId = Guid.NewGuid(),
            Role = new Role { Name = "Administrator" }
        };
        var sut = CreateSut(new FakeUserRepository(user));

        var result = await sut.RequestResetAsync(user.Username);

        Assert.Equal(PasswordResetRequestStatus.CannotReset, result.Status);
    }

    [Fact]
    public async Task RequestResetAsync_UserWithEmployeeEmail_ReturnsSentWithPin()
    {
        var user = CreateUserWithEmployee();
        var sut = CreateSut(new FakeUserRepository(user));

        var result = await sut.RequestResetAsync(user.Username);

        Assert.Equal(PasswordResetRequestStatus.Sent, result.Status);
        Assert.Equal(user.Employee!.Email, result.Email);
        Assert.False(string.IsNullOrEmpty(result.RawPin));
    }

    [Fact]
    public async Task VerifyCodeAsync_WithCorrectPin_ReturnsResetToken()
    {
        var user = CreateUserWithEmployee();
        var codes = new FakePasswordResetCodeRepository();
        var sut = CreateSut(new FakeUserRepository(user), codes);

        var requested = await sut.RequestResetAsync(user.Username);
        var verified = await sut.VerifyCodeAsync(user.Username, requested.RawPin!);

        Assert.True(verified.Success);
        Assert.False(string.IsNullOrEmpty(verified.ResetToken));
    }

    [Fact]
    public async Task VerifyCodeAsync_WithWrongPin_IncrementsAttemptCount()
    {
        var user = CreateUserWithEmployee();
        var codes = new FakePasswordResetCodeRepository();
        var sut = CreateSut(new FakeUserRepository(user), codes);

        await sut.RequestResetAsync(user.Username);
        var verified = await sut.VerifyCodeAsync(user.Username, "000000");

        Assert.False(verified.Success);
        Assert.Equal("invalid_or_expired", verified.FailureReason);
    }

    [Fact]
    public async Task VerifyCodeAsync_ThirdWrongAttempt_InvalidatesCode()
    {
        var user = CreateUserWithEmployee();
        var codes = new FakePasswordResetCodeRepository();
        var sut = CreateSut(new FakeUserRepository(user), codes);

        await sut.RequestResetAsync(user.Username);
        await sut.VerifyCodeAsync(user.Username, "000000");
        await sut.VerifyCodeAsync(user.Username, "000000");
        var thirdAttempt = await sut.VerifyCodeAsync(user.Username, "000000");

        Assert.False(thirdAttempt.Success);
        Assert.Equal("too_many_attempts", thirdAttempt.FailureReason);

        // Auch der eigentlich korrekte PIN wird jetzt nicht mehr akzeptiert.
        var correctPinTooLate = await sut.VerifyCodeAsync(user.Username, "123456");
        Assert.False(correctPinTooLate.Success);
    }

    [Fact]
    public async Task VerifyCodeAsync_ExpiredCode_Fails()
    {
        var user = CreateUserWithEmployee();
        var codes = new FakePasswordResetCodeRepository();
        var sut = CreateSut(new FakeUserRepository(user), codes);

        var requested = await sut.RequestResetAsync(user.Username);
        var stored = await codes.GetLatestByUserIdAsync(user.Id);
        stored!.ExpiresAt = DateTime.UtcNow.AddMinutes(-1);

        var verified = await sut.VerifyCodeAsync(user.Username, requested.RawPin!);

        Assert.False(verified.Success);
        Assert.Equal("invalid_or_expired", verified.FailureReason);
    }

    [Fact]
    public async Task ResetPasswordAsync_WithValidResetToken_ChangesPasswordAndRevokesSessions()
    {
        var user = CreateUserWithEmployee();
        var codes = new FakePasswordResetCodeRepository();
        var refreshTokens = new FakeRefreshTokenRepository();
        await refreshTokens.AddAsync(new RefreshToken
        {
            UserId = user.Id,
            User = user,
            TokenHash = "existing-session-hash",
            ExpiresAt = DateTime.UtcNow.AddDays(1)
        });
        var sut = CreateSut(new FakeUserRepository(user), codes, refreshTokens: refreshTokens);

        var requested = await sut.RequestResetAsync(user.Username);
        var verified = await sut.VerifyCodeAsync(user.Username, requested.RawPin!);
        var oldSecurityStamp = user.SecurityStamp;

        var result = await sut.ResetPasswordAsync(verified.ResetToken!, "neuesPasswort");

        Assert.Equal(ResetPasswordResult.Success, result);
        Assert.Equal("hashed:neuesPasswort", user.PasswordHash);
        Assert.NotEqual(oldSecurityStamp, user.SecurityStamp);

        var existingSession = await refreshTokens.GetByTokenHashAsync("existing-session-hash");
        Assert.NotNull(existingSession!.RevokedAt);
    }

    [Fact]
    public async Task ResetPasswordAsync_ResetTokenIsSingleUse()
    {
        var user = CreateUserWithEmployee();
        var codes = new FakePasswordResetCodeRepository();
        var sut = CreateSut(new FakeUserRepository(user), codes);

        var requested = await sut.RequestResetAsync(user.Username);
        var verified = await sut.VerifyCodeAsync(user.Username, requested.RawPin!);

        var first = await sut.ResetPasswordAsync(verified.ResetToken!, "erstesPasswort");
        var second = await sut.ResetPasswordAsync(verified.ResetToken!, "zweitesPasswort");

        Assert.Equal(ResetPasswordResult.Success, first);
        Assert.Equal(ResetPasswordResult.InvalidOrExpiredToken, second);
    }

    [Fact]
    public async Task ResetPasswordAsync_NewPasswordTooShort_Fails()
    {
        var user = CreateUserWithEmployee();
        var codes = new FakePasswordResetCodeRepository();
        var sut = CreateSut(new FakeUserRepository(user), codes);

        var requested = await sut.RequestResetAsync(user.Username);
        var verified = await sut.VerifyCodeAsync(user.Username, requested.RawPin!);

        var result = await sut.ResetPasswordAsync(verified.ResetToken!, "kurz");

        Assert.Equal(ResetPasswordResult.PasswordTooShort, result);
        Assert.Equal("hashed:secret", user.PasswordHash);
    }

    [Fact]
    public async Task IssueInviteCodeAsync_ReturnsPin_WithRequestedValidity()
    {
        var user = CreateUserWithEmployee();
        var codes = new FakePasswordResetCodeRepository();
        var sut = CreateSut(new FakeUserRepository(user), codes);

        var rawPin = await sut.IssueInviteCodeAsync(user.Id, TimeSpan.FromDays(7));

        Assert.False(string.IsNullOrEmpty(rawPin));
        var stored = await codes.GetLatestByUserIdAsync(user.Id);
        Assert.NotNull(stored);
        Assert.True(stored!.ExpiresAt > DateTime.UtcNow.AddDays(6));
        Assert.True(stored.ExpiresAt < DateTime.UtcNow.AddDays(8));
    }

    [Fact]
    public async Task IssueInviteCodeAsync_ReIssuing_StillProducesAVerifiableCode()
    {
        var user = CreateUserWithEmployee();
        var codes = new FakePasswordResetCodeRepository();
        var sut = CreateSut(new FakeUserRepository(user), codes);

        await sut.IssueInviteCodeAsync(user.Id, TimeSpan.FromDays(7));
        var secondPin = await sut.IssueInviteCodeAsync(user.Id, TimeSpan.FromDays(7));

        var verified = await sut.VerifyCodeAsync(user.Username, secondPin);
        Assert.True(verified.Success);
    }
}
