using Microsoft.Extensions.Caching.Memory;
using OmsorgCore.Application.Services;
using OmsorgCore.Domain.Entities;
using OmsorgCore.Tests.TestDoubles;

namespace OmsorgCore.Tests.Services;

public class AuthServiceTests
{
    private static User CreateUser(string username = "sabina", string passwordHash = "hashed:secret") => new()
    {
        Username = username,
        PasswordHash = passwordHash,
        IsActive = true,
        RoleId = Guid.NewGuid(),
        Role = new Role { Name = "Geschaeftsfuehrung" }
    };

    private const string TestIp = "127.0.0.1";

    private static AuthService CreateSut(
        FakeUserRepository? users = null,
        FakeRefreshTokenRepository? refreshTokens = null,
        FakeLoginAttemptRepository? loginAttempts = null,
        FakeLoginLockoutPolicy? lockoutPolicy = null)
    {
        var userRepository = users ?? new FakeUserRepository();
        return new AuthService(
            userRepository,
            refreshTokens ?? new FakeRefreshTokenRepository(),
            new FakePasswordHasher(),
            new FakeJwtTokenGenerator(),
            new FakeRefreshTokenGenerator(),
            new PermissionService(userRepository, new MemoryCache(new MemoryCacheOptions())),
            loginAttempts ?? new FakeLoginAttemptRepository(),
            lockoutPolicy ?? new FakeLoginLockoutPolicy());
    }

    [Fact]
    public async Task LoginAsync_WithValidCredentials_ReturnsAccessAndRefreshToken()
    {
        var user = CreateUser();
        var users = new FakeUserRepository(user);
        var sut = CreateSut(users);

        var result = await sut.LoginAsync(user.Username, "secret", TestIp);

        Assert.True(result.Success);
        Assert.False(string.IsNullOrEmpty(result.Token));
        Assert.False(string.IsNullOrEmpty(result.RefreshToken));
        Assert.NotNull(result.ExpiresAt);
        Assert.Equal(user.Id, result.UserId);
        Assert.Equal(user.Username, result.Username);
    }

    [Fact]
    public async Task LoginAsync_WithWrongPassword_Fails()
    {
        var user = CreateUser();
        var users = new FakeUserRepository(user);
        var sut = CreateSut(users);

        var result = await sut.LoginAsync(user.Username, "wrong-password", TestIp);

        Assert.False(result.Success);
        Assert.Null(result.RefreshToken);
    }

    [Fact]
    public async Task RefreshAsync_WithValidToken_RotatesAndIssuesNewPair()
    {
        var user = CreateUser();
        var users = new FakeUserRepository(user);
        var refreshTokens = new FakeRefreshTokenRepository();
        var sut = CreateSut(users, refreshTokens);

        var login = await sut.LoginAsync(user.Username, "secret", TestIp);
        var refreshed = await sut.RefreshAsync(login.RefreshToken!);

        Assert.True(refreshed.Success);
        Assert.NotEqual(login.RefreshToken, refreshed.RefreshToken);
        Assert.NotEqual(login.Token, refreshed.Token);

        var oldTokenHash = new FakeRefreshTokenGenerator().HashToken(login.RefreshToken!);
        var oldToken = await refreshTokens.GetByTokenHashAsync(oldTokenHash);
        Assert.NotNull(oldToken!.RevokedAt);
        Assert.NotNull(oldToken.ReplacedByTokenId);
    }

    [Fact]
    public async Task RefreshAsync_WithRevokedToken_Fails()
    {
        var user = CreateUser();
        var users = new FakeUserRepository(user);
        var refreshTokens = new FakeRefreshTokenRepository();
        var sut = CreateSut(users, refreshTokens);

        var login = await sut.LoginAsync(user.Username, "secret", TestIp);
        await sut.RevokeAsync(login.RefreshToken!);

        var refreshed = await sut.RefreshAsync(login.RefreshToken!);

        Assert.False(refreshed.Success);
    }

    [Fact]
    public async Task RefreshAsync_WithExpiredToken_Fails()
    {
        var user = CreateUser();
        var users = new FakeUserRepository(user);
        var refreshTokens = new FakeRefreshTokenRepository();
        var sut = CreateSut(users, refreshTokens);

        var login = await sut.LoginAsync(user.Username, "secret", TestIp);
        var tokenHash = new FakeRefreshTokenGenerator().HashToken(login.RefreshToken!);
        var stored = await refreshTokens.GetByTokenHashAsync(tokenHash);
        stored!.ExpiresAt = DateTime.UtcNow.AddMinutes(-1);

        var refreshed = await sut.RefreshAsync(login.RefreshToken!);

        Assert.False(refreshed.Success);
    }

    [Fact]
    public async Task RefreshAsync_WithUnknownToken_Fails()
    {
        var sut = CreateSut();

        var refreshed = await sut.RefreshAsync("does-not-exist");

        Assert.False(refreshed.Success);
    }

    [Fact]
    public async Task LoginAsync_RevokesPreviousActiveSessions()
    {
        var user = CreateUser();
        var users = new FakeUserRepository(user);
        var refreshTokens = new FakeRefreshTokenRepository();
        var sut = CreateSut(users, refreshTokens);

        var firstLogin = await sut.LoginAsync(user.Username, "secret", TestIp);
        var secondLogin = await sut.LoginAsync(user.Username, "secret", TestIp);

        var firstTokenRefreshed = await sut.RefreshAsync(firstLogin.RefreshToken!);
        Assert.False(firstTokenRefreshed.Success);

        var secondTokenRefreshed = await sut.RefreshAsync(secondLogin.RefreshToken!);
        Assert.True(secondTokenRefreshed.Success);
    }

    [Fact]
    public async Task RevokeAsync_IsIdempotent()
    {
        var user = CreateUser();
        var users = new FakeUserRepository(user);
        var sut = CreateSut(users);

        var login = await sut.LoginAsync(user.Username, "secret", TestIp);

        await sut.RevokeAsync(login.RefreshToken!);
        await sut.RevokeAsync(login.RefreshToken!);

        var refreshed = await sut.RefreshAsync(login.RefreshToken!);
        Assert.False(refreshed.Success);
    }

    [Fact]
    public async Task LoginAsync_AfterMaxFailures_LocksOutEvenWithCorrectPassword()
    {
        var user = CreateUser();
        var users = new FakeUserRepository(user);
        var lockoutPolicy = new FakeLoginLockoutPolicy { MaxFailures = 3 };
        var sut = CreateSut(users, lockoutPolicy: lockoutPolicy);

        for (var i = 0; i < 3; i++)
        {
            await sut.LoginAsync(user.Username, "wrong-password", TestIp);
        }

        var result = await sut.LoginAsync(user.Username, "secret", TestIp);

        Assert.False(result.Success);
        Assert.True(result.IsLockedOut);
    }

    [Fact]
    public async Task LoginAsync_SuccessfulLogin_ClearsPriorFailures()
    {
        var user = CreateUser();
        var users = new FakeUserRepository(user);
        var lockoutPolicy = new FakeLoginLockoutPolicy { MaxFailures = 2 };
        var sut = CreateSut(users, lockoutPolicy: lockoutPolicy);

        await sut.LoginAsync(user.Username, "wrong-password", TestIp);
        var successfulLogin = await sut.LoginAsync(user.Username, "secret", TestIp);
        Assert.True(successfulLogin.Success);

        var nextLogin = await sut.LoginAsync(user.Username, "secret", TestIp);
        Assert.True(nextLogin.Success);
        Assert.False(nextLogin.IsLockedOut);
    }

    [Fact]
    public async Task LoginAsync_DifferentIp_IsNotAffectedByOtherIpsFailures()
    {
        var user = CreateUser();
        var users = new FakeUserRepository(user);
        var lockoutPolicy = new FakeLoginLockoutPolicy { MaxFailures = 1 };
        var sut = CreateSut(users, lockoutPolicy: lockoutPolicy);

        await sut.LoginAsync(user.Username, "wrong-password", "10.0.0.1");
        var result = await sut.LoginAsync(user.Username, "secret", "10.0.0.2");

        Assert.True(result.Success);
    }
}
